ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

电信客户流失预测实战:朴素贝叶斯建模与避坑指南

2026/9/26 11:27:36 拓冰建站 浏览量
电信客户流失预测实战:朴素贝叶斯建模与避坑指南 简介面向数据挖掘初学者及相关课程设计场景基于朴素贝叶斯算法完成电信客户流失预测建模资源包含Kaggle公开客户数据集、完整Python代码与8000字实验报告。数据共7043条、21个属性按客户个人信息、账户信息、订阅服务和流失评价指标四类划分可直接用于特征分析与模型训练。压缩包共9个文件、约3.1MB其中csv/tsv为原始数据与结果ipynb和html对应可运行代码与可视化输出docx为详细实验报告json与tfevents记录模型训练过程整体结构清晰。目前已有297人学习下载适合需要从数据预处理、特征工程到算法评估完整实践朴素贝叶斯流程的读者由此可快速搭建电信客户流失预测基线模型并能根据报告中的思路扩展改进。1. 电信客户流失预测为什么用朴素贝叶斯而不是先上深度学习一个做数据挖掘的初学者拿到“电信客户流失分析预测”这个题目最容易踩的坑是上来就套 XGBoost 或者神经网络等到写实验报告时才发现解释不了每一层在干什么参数也调不明白。标题里点名的朴素贝叶斯算法恰好是这类场景里性价比最高的起点数据量不大、特征有明确的业务含义、需要交代清楚每个特征的贡献。它不会给你刷出一个惊人的准确率但能让你的实验报告从“调包”变成“真懂”。这篇文章就围绕数据集、代码、实验报告这三个交付物把从原始用户表到一份能交差的预测模型完整拆开包括数据预处理、模型选型、代码实现、参数调整和最常见的翻车点。适合正在做课程设计、毕业设计或想快速验证一个数据挖掘流程的从业者。2. 数据集与预处理从原始用户表到能喂进朴素贝叶斯的样子2.1 电信客户流失数据集长什么样常见字段与目标变量电信客户流失预测这个题目在公开数据集里非常常见比如某电信公司用户档案通常包含几十个字段目标变量是Churn是否流失。典型字段包括用户ID、性别、是否老年人、是否配偶或家属同住是否开通电话服务、是否开通多条线路网络服务类型光纤、DSL、无网络在线安全、在线备份、设备保护、技术支持、流媒体电视、流媒体电影等是否开通合同类型按月、一年、两年付款方式电子支票、邮寄支票、银行自动转账、信用卡自动转账月费用、总费用流失标签是/否这些字段里有大量是“是否”型Yes/No少量是类别型如合同类型还有两个连续数值月费用、总费用。给朴素贝叶斯用必须先把它们都转成数值。很多人直接拿原始字符串跑LabelEncoder把“Yes”和“No”变成 1 和 0看起来没问题但遇到InternetService这种有三种取值的字段LabelEncoder会编成 0、1、2朴素贝叶斯会认为 2 和 0 之间是“两倍远”这本质上是强加了一个不存在的顺序关系。正确做法是一律做 One-Hot 编码或者直接用pd.get_dummies。2.2 清洗与编码缺失值、字符串列、数值分布先说缺失值。电信数据集里最常见的坑是TotalCharges字段看起来是数值实际上读进来是字符串里面可能有空字符串或字母。我一般用pd.to_numeric转一次errorscoerce会把非法值变成NaN然后看缺失比例。如果缺失比例小于 1%直接删除这些行如果超过 5%就要考虑用中位数填充因为TotalCharges往往右偏均值会被高额用户拉高。import pandas as pd df pd.read_csv(telecom_customer_churn.csv) df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) print(df.isnull().sum()) # 流失用户里 TotalCharges 缺失的情况 print(df[df[Churn] Yes][[TotalCharges]].isnull().sum()) # 如果缺失很少直接删除如果较多用中位数填充 df df.dropna(subset[TotalCharges]) df[TotalCharges] df[TotalCharges].fillna(df[TotalCharges].median())这段代码的逻辑是先用errorscoerce把非数值变成 NaN再看缺失数量。很多公开数据集里TotalCharges对于刚入网一个月的用户本来就是 0不能删掉否则会系统性丢掉新用户样本。填充时用中位数而不是均值是因为这个字段长尾严重少数老用户贡献了极高费用均值会偏移。接下来处理字符串类别特征。我的做法是把目标变量和特征分开特征做 One-Hot# 目标变量转 0/1 df[Churn] df[Churn].map({Yes: 1, No: 0}) # 找出所有 object 类型的列 obj_cols df.select_dtypes(include[object]).columns.tolist() obj_cols [c for c in obj_cols if c ! Churn] # 对特征做独热编码 df_encoded pd.get_dummies(df, columnsobj_cols, drop_firstFalse)这里drop_firstFalse是故意的。朴素贝叶斯不要求特征之间线性无关保留全部哑变量反而能让每个类别的概率估计更直接。如果drop_firstTrue第一个类别被丢掉模型输出概率时少了一个维度解释起来麻烦。2.3 特征工程哪些特征对流失预测真的有区分度特征工程不是越花哨越好电信流失预测里真正有区分度的往往就那几个合同类型、月费用、总费用、开通服务数量。我的经验是先做一个简单的单变量分析把每个特征按不同取值统计流失率看差异。这里给出一个可复制的交叉表方法# 按合同类型看流失率 churn_by_contract df.groupby(Contract)[Churn].mean() print(churn_by_contract) # 按月费用分箱看流失率 df[MonthlyChargesBin] pd.cut(df[MonthlyCharges], bins5) print(df.groupby(MonthlyChargesBin)[Churn].mean())如果Contract里“按月”合同的流失率明显高于“两年”合同说明合同期限是强特征。连续变量分箱后看趋势能帮你决定是直接用原始数值进高斯朴素贝叶斯还是先离散化再用多项式朴素贝叶斯。实际上高斯朴素贝叶斯对连续特征直接建模正态分布但如果特征明显双峰或长尾效果会差。这时候可以对MonthlyCharges做分箱转成有序类别再用多项式变体。分箱的边界不要用等宽而是用分位数否则极端值会把多数样本挤到一个箱里。还有一个容易忽略的点TotalCharges和MonthlyCharges高度相关因为总费用约等于月费乘以在网月数。两个强相关特征放进朴素贝叶斯会放大条件独立的假设误差。我的做法是构造一个衍生特征TenureMonths TotalCharges / MonthlyCharges在网月数然后舍掉原始总费用。这个衍生特征业务含义清晰而且比原始两个相关特征更稳。3. 朴素贝叶斯原理与建模先搞懂条件独立假设再谈调参3.1 朴素贝叶斯的三种变体高斯、伯努利、多项式怎么选朴素贝叶斯的核心是通过贝叶斯定理计算后验概率然后默认特征之间条件独立。这个“朴素”假设在现实中几乎不成立但流失预测这种业务场景里特征之间的相关性相对弱模型依然能给出不错的排序能力。实现上sklearn提供了三个常用变体GaussianNB特征服从高斯分布适合连续数值BernoulliNB特征都是二值的适合“是否开通”这种特征MultinomialNB特征取值是计数或频次适合文本、离散计数电信流失数据里既有连续值又有二值特征怎么选我的常见做法是如果只做快速基线直接用GaussianNB把所有特征都当成连续值。缺点是二值特征会被强行拟合成高斯分布但结果往往还能接受。如果追求严谨把二值特征单独拆出来用BernoulliNB连续特征单独处理后拼概率。但sklearn没有现成的混合朴素贝叶斯所以大部分课程设计都用GaussianNB一把梭。我的建议是先跑通GaussianNB做基线再看验证集表现决定要不要拆特征。多数情况你会发现GaussianNB的准确率在 75%~80%已经够写实验报告了。3.2 用 sklearn 构建基线模型训练集/测试集划分与评估指标搭建一个最小可用模型只需要几行代码。先切分数据再做标准化然后训练。标准化对朴素贝叶斯很重要虽然算法本身基于概率分布不依赖距离但高斯朴素贝叶斯估计均值方差时如果量纲差异过大比如月费用几十、总费用几千数值稳定性会受影响。from sklearn.model_selection import train_test_split from sklearn.naive_bayes import GaussianNB from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix X df_encoded.drop(Churn, axis1) y df_encoded[Churn] # 划分训练集和测试集保持类别分布一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 标准化连续特征这里假设所有特征都标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model GaussianNB() model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))代码里的stratifyy是必须的。电信流失数据里“流失”通常只占 20%~30%如果不分层抽样随机划分可能让测试集里流失样本占比变成 10%模型评估指标会失真。random_state42固定随机种子保证实验报告结果可复现。标准化时要先fit_transform训练集再transform测试集不能用测试集去fit否则就是数据泄露。3.3 参数与先验平滑系数、类先验、样本权重的影响朴素贝叶斯的超参数不多但每个都有实际意义。GaussianNB里有priors参数可以手动指定类别先验概率。默认情况下模型会从训练数据里估计先验也就是训练集中流失样本的占比。如果你的训练集经过采样导致流失比例被改变就要手动传入真实先验。# 假设业务方告诉你真实流失率是 25% model_prior GaussianNB(priors[0.75, 0.25]) model_prior.fit(X_train_scaled, y_train)MultinomialNB里有alpha平滑系数默认是 1.0也就是拉普拉斯平滑。当某个特征在某个类别下从未出现过时概率为 0 会把整个乘积打成 0平滑就是给所有计数加一个小数避免零概率。alpha太小会过拟合太大则所有概率趋向均匀。我通常用网格搜索在[0.01, 0.1, 1.0, 10]里选。BernoulliNB的参数是binarize决定把特征大于多少变成 1小于等于变成 0。如果你的数据是连续值但想用伯努利版本这个阈值需要调。默认 0.0但标准化后均值为 0实际会变成约一半 1 一半 0你需要根据业务含义设置。比如月费用大于 50 才算“高费用”特征就手动传binarize50但前提是没做标准化。4. 代码实现与落地完整流程从训练到预测4.1 最小可运行代码数据加载、模型训练、结果输出如果你拿到的是标题里那种“数据集代码实验报告”的压缩包里面大概率已经有一个.py或.ipynb文件。但自己做一遍才是正经事。这里我给出一套能直接跑通的最小流程假设你已经把数据清理成df_encoded。import pandas as pd from sklearn.model_selection import train_test_split from sklearn.naive_bayes import GaussianNB, BernoulliNB from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score, accuracy_score def load_and_prepare(csv_path): df pd.read_csv(csv_path) df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) df df.dropna(subset[TotalCharges]) df[Churn] df[Churn].map({Yes: 1, No: 0}) obj_cols df.select_dtypes(include[object]).columns.drop(Churn) df pd.get_dummies(df, columnsobj_cols, drop_firstFalse) return df df load_and_prepare(telecom_customer_churn.csv) X df.drop(Churn, axis1) y df[Churn] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) model GaussianNB() model.fit(X_train, y_train) y_pred model.predict(X_test) y_prob model.predict_proba(X_test)[:, 1] print(准确率:, accuracy_score(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_prob))这个函数把数据准备和建模封装在一起方便你在实验报告里贴。关键点是predict_proba拿到的第二列是“流失”的概率做 ROC 曲线时用概率而不是预测标签。AUC 比准确率更能反映排序能力因为流失预测的核心是找出高风险用户而不是把所有人都猜成“不流失”。4.2 交叉验证与网格搜索把准确率压榨到合理区间单次划分训练测试集容易受随机波动影响实验报告里最好加 5 折交叉验证。朴素贝叶斯训练速度快交叉验证几乎没有成本。我一般用cross_val_score输出平均分和标准差。from sklearn.model_selection import cross_val_score, GridSearchCV from sklearn.naive_bayes import MultinomialNB # 如果特征是离散的用 MultinomialNB 做交叉验证 model_mnb MultinomialNB() scores cross_val_score(model_mnb, X_train, y_train, cv5, scoringroc_auc) print(5折 AUC 均值:, scores.mean(), 标准差:, scores.std()) # 网格搜索平滑系数 param_grid {alpha: [0.01, 0.1, 1.0, 10]} grid GridSearchCV(MultinomialNB(), param_grid, cv5, scoringroc_auc) grid.fit(X_train, y_train) print(最佳 alpha:, grid.best_params_) print(最佳 AUC:, grid.best_score_)注意这里我用的是X_train但网格搜索内部会再做交叉验证。如果你把X_train和X_test都传进去测试集就被污染了。网格里的alpha从 0.01 到 10 指数增长是因为平滑系数的效果是数量级变化的线性搜索没有意义。scoringroc_auc比默认的准确率更适合不平衡数据。还有一个容易被忽略的参数是class_prior。如果数据集里流失用户很少模型会把所有样本都预测成“不流失”准确率可能有 80%但 AUC 只有 0.5。这时候可以给模型传入类先验或者用predict_proba输出概率后自己调整判定阈值。朴素贝叶斯并没有内置的阈值参数但你可以这样用y_prob model.predict_proba(X_test)[:, 1] # 用自定义阈值 0.3 判定流失 y_custom (y_prob 0.3).astype(int)把阈值从默认的 0.5 降到 0.3会抓出更多潜在流失用户虽然误报增加但业务上挽回一个用户的收益远大于打扰一个不流失用户的成本。实验报告里如果能写出“按业务风险调整阈值”会显得你考虑过落地而不是只跑了一个模型。4.3 输出可解释结果预测概率与特征贡献朴素贝叶斯相对树模型的最大优势是概率输出可以直接查表。GaussianNB训练完后每个特征在“流失”和“不流失”两个类别下都有一组均值和方差这就是模型的“参数说明”。你可以把这些参数打印出来分析哪些特征对判断流失影响最大。import numpy as np # 获取每个特征在每个类别下的均值和方差 # 注意 X_train 是标准化后的所以要映射回原始特征名 sigma np.sqrt(model.var_) mean_diff model.theta_[1] - model.theta_[0] # 流失类均值 - 不流失类均值 # 按均值差异排序找到最有区分度的特征 feature_names X.columns.tolist() ranking sorted(zip(feature_names, mean_diff), keylambda x: abs(x[1]), reverseTrue) for name, diff in ranking[:10]: print(f{name}: {diff:.4f})这里有一个坑model.theta_是按标准化后的尺度算的均值不能直接解释成“月费用增加一美元流失概率增加多少”。如果你要写进实验报告需要对每个特征做业务解释而不是套用模型内部的标准化数值。我的习惯是重新训练一个不加标准化的GaussianNB打印原始尺度的均值和方差再配合上面的排序结果更直观。标准化只为了数值稳定解释模型时用原始输入重训一遍完全不冲突。5. 避坑清单做电信客户流失分析最常见的 5 个翻车现场5.1 数据泄露把未来的信息放进训练集现象训练出来的 AUC 高达 0.95怎么看都像假数据。原因数据里有“总费用”字段它是在用户整个生命周期结束后统计的。如果一个用户已经流失总费用就是流失前的全部费用如果还没流失总费用就是截至今天。模型直接学到了“总费用高的人还没走”而不是“什么样的人会走”。解决要么把TotalCharges换成MonthlyCharges * TenureMonths要么只用MonthlyCharges和合同类型不要动总费用。更隐蔽的还有tenure字段它在预测时是已知的但如果业务上是要预测未来三个月内是否流失tenure是当前的合约时长这个可以用。真正不能用的是“事后统计”字段比如“已经使用过的服务次数”“投诉次数”如果这些是在流失后才汇总的就是泄露。5.2 类别不平衡准确率虚高而召回率惨不忍睹现象测试集准确率 82%但一看混淆矩阵流失用户一个都没抓到全部预测成“不流失”。原因流失样本只占 25%默认模型只要全猜“不流失”就能有 75% 准确率。解决不用准确率评估改用 ROC-AUC、召回率、F1。训练层面可以做 SMOTE 过采样或者给模型加样本权重。朴素贝叶斯没有class_weight参数但你可以对训练集重采样。最简单的操作是train_test_split时用stratify不要手工抽样。实验报告里建议同时给出混淆矩阵和不同阈值下的精确率-召回率曲线这样才能看出模型在“尽力抓流失”和“避免误报”两个方向上的取舍。5.3 字符串特征编码顺序导致模型假设被破坏现象LabelEncoder把Contract变成 0、1、2模型效果突然变差。原因0代表“按月”1代表“一年”2代表“两年”朴素贝叶斯不知道这个顺序它只会机械计算每个数值在两类中的分布。如果编码顺序和业务顺序不一致比如“两年”被编成 1“一年”被编成 2模型就把两个类别的位置颠倒了概率估计会受到干扰。解决类别特征一律用OneHotEncoder或pd.get_dummies。如果一定要用标签编码只对有序类别如Contract使用并且手动指定顺序。对于InternetService这种纯名义变量绝对不能标签编码。5.4 报告与代码不一致实验报告里写的结果复现不出来现象实验报告截图显示 AUC 0.86但自己跑代码只有 0.78。原因报告可能用了不同版本的包、不同的随机种子或者删除了某些离群样本再或者报告里的模型是调参后的最优结果而代码里写的是基线。解决实验报告里必须固定random_state并写明所有关键参数包括测试集比例、是否标准化、数据清洗规则。压缩包里的代码如果有random_state42你也用 42。如果你发现报告结果复现不了不要直接改大数字而是检查数据预处理步骤是不是少了一步。比如报告里可能先删除了tenure0的用户代码里却没删结果差异就来了。这是血泪经验。5.5 混淆矩阵和 ROC 曲线只看一个指标现象只贴一张 ROC 曲线图算了个 AUC 就收工。原因ROC 曲线对类别不平衡不敏感正样本即使只有 1%曲线还是很“漂亮”。但业务上你需要知道具体哪些用户被误判。解决画出混淆矩阵并计算“流失用户的召回率”和“不流失用户的误报率”这两个数字。下面这段代码可以帮你快速输出关键指标from sklearn.metrics import recall_score, precision_score, f1_score print(流失用户召回率:, recall_score(y_test, y_pred, pos_label1)) print(流失用户精确率:, precision_score(y_test, y_pred, pos_label1)) print(F1:, f1_score(y_test, y_pred, pos_label1))如果召回率低于 0.5说明模型对真正流失的用户没有敏感度这个模型在业务上基本不可用。这时候要回头检查特征而不是继续调参。6. 验证模型与进阶从跑通代码到讲清楚业务价值6.1 用留存验证集和线下评估确认模型稳定性很多人把训练测试集切一次就结束了我更习惯再留一份“留存验证集”。具体做法是先把全量数据按时间排序用最后 20% 的用户作为留存集前面的 80% 再按随机方式切训练和验证。电信用户流失有明显的月份周期性如果用随机划分训练集和测试集可能混着同一时期的用户模型会记住那个时期的营销活动特征。按时间切分才能模拟真实的“用上个月数据预测下个月流失”场景。df_time df_encoded.sort_values(tenure, ascendingFalse) # 假设 tenure 大的用户更早入网取最后 20% 作为留存验证 split_idx int(len(df_time) * 0.8) train_part df_time.iloc[:split_idx] holdout df_time.iloc[split_idx:] # 训练部分内部再切 80/20 X_tr, X_val, y_tr, y_val train_test_split( train_part.drop(Churn, axis1), train_part[Churn], test_size0.2, random_state42, stratifytrain_part[Churn] ) # 训练、调参后用 holdout 做最终验证按tenure排序并不是严格按时间但在没有真实时间戳的数据集里这个近似比随机划分更可靠。如果数据里有tenure字段业务上是“在网月数”它可以近似代表用户入网早晚。注意不要把tenure放进特征里再排序排序后特征不变只是划分不同。用留存集验证时如果 AUC 比验证集低不少大概率是过拟合了这时候需要减少特征或者增加正则化朴素贝叶斯这边就是加大平滑系数。6.2 把预测结果映射成挽留策略价值分层与行动阈值模型最终不是用来发论文的而是要告诉运营团队“这 1000 个用户下个月可能流失建议优先挽留这 100 个。”操作上我把预测概率按从高到低排序再结合用户月费用做一个价值分层results pd.DataFrame({ customer_id: df_time[customerID].values[split_idx:], churn_prob: model.predict_proba(holdout_scaled)[:, 1], monthly_charges: holdout[MonthlyCharges].values }) # 高风险 流失概率大于 0.6 且月费高于中位数 high_value_risk results[ (results[churn_prob] 0.6) (results[monthly_charges] results[monthly_charges].median()) ].sort_values(churn_prob, ascendingFalse) print(高风险高价值用户数:, len(high_value_risk))这个逻辑的价值在于它把机器学习输出和业务成本直接挂钩。给一个月费 20 的低价值用户发优惠券成本可能超过收益但一个月费 100 的两年合同用户值得用专属客服去谈续约。实验报告里如果能加一张“风险分层表”说明不同阈值下需要投入的挽留预算整个项目的说服力会强很多。这也是我做完这个分析后最想对新手说的一件事模型准确率不是交差指标能不能告诉业务方“下一周该给谁打电话”才是这个项目值不值得做的标准。希望这些细节能帮你在自己的电信数据集上少走几步弯路。本文还有配套的精品资源点击获取