ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

电信客户流失预测:数据清洗、特征可视化与建模调参实战

电信客户流失预测:数据清洗、特征可视化与建模调参实战 简介面向数据科学与机器学习初学者的电信客户流失预测完整项目主要解决运营商如何根据客户属性与使用行为判断其是否会流失的问题。项目覆盖数据探索、特征可视化、分类模型训练与未知样本预测并包含完整源码与说明文档适合作为课程设计、毕业设计或入门级实战练习。压缩包内共十五个文件含Python源码、CSV数据集、训练好的sav模型、项目说明及九张PNG结果图整体仅五百四十五KB便于下载后快速核对运行。源码经过测试可直接执行说明文档要素清晰结果图涵盖ROC曲线、混淆矩阵、特征频率、客户服务通话次数与流失关系、是否开通国际套餐与流失对比等可帮助理解特征工程、模型评估与预测流程。目前已有167人学习浏览对计算机、数据科学、通信等专业学生完成大作业或初期项目演示有较高借鉴价值。1. 电信客户流失预测为什么准确率90%的模型可能毫无用处客户流失预测是电信公司最日常的数据分析任务之一。我见过不少刚入门的同学拿着这份数据很快把准确率跑到了 90%兴冲冲拿去汇报结果业务方问一句“那你能告诉我哪个客户要流失吗”就答不上来——因为流失客户只占数据集的 26%模型哪怕把所有人都预测成“不流失”准确率也有 74%。这份项目要解决的不是把准确率刷高而是顺着数据清洗、特征可视化、模型预测这条线把“哪些客户会流失”从猜测变成可解释的概率。它适合想完整跑一遍 python 数据分析与可视化流程的人也适合需要在业务汇报里讲清楚流失原因的分析师。2. 清洗电信客户数据从原始 CSV 到可建模的干净 DataFrame2.1 字段盘点先看清 21 个字段里哪些能进模型哪些是噪音拿到这类带“python源码项目说明”的工程包第一件事不是解压跑代码而是先读项目说明里对数据集的描述再对着实际数据做字段盘点。电信客户流失数据业内常用的是 Telco 版本一份 CSV 大概 7000 行、21 列每行是一个客户最后一列 Churn 是标签。用 pandas 读进来之后别急着建模先做两件事df.info() 看每一列的类型和缺失情况df.nunique() 看每个字段有多少种取值。import pandas as pd df pd.read_csv(Telco-Customer-Churn.csv) print(df.shape) print(df.info()) print(df.nunique())这段输出会告诉你几件事customerID 每行唯一不能进模型tenure、MonthlyCharges、TotalCharges 是连续值剩下的字段如 Contract、PaymentMethod、InternetService 是多类别性别、是否老人这类二值字段也可以用 0/1 编码。我一般会把字段分成三类直接可用的数值、要编码的类别、明显不该进模型的噪音。这一步不该省因为后面编码和可视化都以这里的字段清单为基础项目说明里的字段含义表要和实际列名一一对应防止脚本里下标写错。2.2 把 TotalCharges 转成 float、把 Churn 映射成 0/1最小清洗脚本这份数据最经典的坑在 TotalCharges 这一列它在 CSV 里存的是字符串而且部分行是空字符串。直接用 astype(float) 会当场报错。正确做法是用 pd.to_numeric 带 errorscoerce把非数字值统一转成 NaN然后再看有多少行需要处理。df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) print(df[TotalCharges].isna().sum()) df df.dropna(subset[TotalCharges]) df[Churn] df[Churn].map({Yes: 1, No: 0}) df df.drop(columns[customerID])errorscoerce 是文本转数值的标准姿势它会丢掉报错只留 NaN。一般纯缺失只有十来行直接 dropna 就好如果缺了几百行就要考虑用 tenure × MonthlyCharges 去填充因为 TotalCharges 本身就是这两者的累计近似值。Churn 映射成 0/1 之后后面画混淆矩阵、算 AUC 都顺手。还有一点值得注意如果数据里某行的 TotalCharges 明显不等于 tenure 乘 MonthlyCharges那是原始数据录入的问题清洗阶段不用深究。2.3 类别特征编码One-Hot 与“合并语义”的选择剩下性别、互联网服务、合约类型、付款方式等十几个类别字段。最常见的做法是 pandas 的 get_dummies 直接展开一行代码生成几十列。但这里有个极容易被忽视的细节OnlineSecurity、TechSupport 这类字段里有一个取值是“No internet service”它表达的意思是“客户根本没开通网络”和“No”语义不同。如果直接 get_dummies会把两个意思几乎相同的取值拆成两列模型学到一套多余的规则。for col in [OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies]: df[col] df[col].replace(No internet service, No) df pd.get_dummies(df, drop_firstTrue) print(df.shape)先把“No internet service”统一替换成“No”再做 one-hot。drop_firstTrue 对二值字段只生成一列而不是两列避免冗余多类别字段则保留 k-1 列。做完之后 DataFrame 一般会从 20 列左右扩到 40 列左右属于正常现象。如果你习惯用 sklearn 的 OneHotEncoder要注意它返回的是稀疏矩阵需要配合 pd.DataFrame 转回结构化表格否则后面可视化时索引对齐容易出问题。2.4 特征工程把多项增值服务合并成 service_count 减少稀疏编码后维度变多但有一个比单列更稳的手工特征值得加进去这个客户一共开通了几项增值服务。把 OnlineSecurity、OnlineBackup、DeviceProtection、TechSupport、StreamingTV、StreamingMovies 这六个二值字段相加得到 0 到 6 的整数。在业务上这个数字和“客户粘性”直接挂钩实测里服务数量为 0 的客户流失率明显更高而且比单独看某一个服务项更稳定。service_cols [OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies] df[service_count] df[service_cols].sum(axis1)这个特征加了之后即便把原来的六列全删掉模型效果也不会差太多因为增值服务的共性已经被 service_count 提取出来。是否保留原六列取决于模型逻辑回归保留六列会让权重分散、解释困难删掉更干净随机森林保留六列没坏处树模型自己会挑。这也是为什么特征可视化必须放在建模之前——先靠业务直觉构造特征再靠图去验证而不是盲目堆维度。3. 特征可视化用图表定位与流失强相关的特征3.1 数值特征分布tenure 和 MonthlyCharges 的流失差异可视化要按顺序来做先看数值特征再看类别特征最后看相关性。数值特征就三个tenure、MonthlyCharges、TotalCharges。用 seaborn 画三张箱线图按 Churn 分组是速度最快的诊断方式。注意这里用的是清洗后但还没 get_dummies 的副本否则画图时列名全是编码后的名字没法解释。import matplotlib.pyplot as plt import seaborn as sns df_raw df.copy() # 编码前的副本用于画图 fig, axes plt.subplots(1, 3, figsize(14, 4)) sns.boxplot(xChurn, ytenure, datadf_raw, axaxes[0]) sns.boxplot(xChurn, yMonthlyCharges, datadf_raw, axaxes[1]) sns.boxplot(xChurn, yTotalCharges, datadf_raw, axaxes[2]) plt.tight_layout() plt.show()箱线图能一眼看出结论流失客户的 tenure 中位数明显低于留存客户说明新客户更容易流失MonthlyCharges 在流失组略高但两组箱体重叠很大单靠月费判断不了TotalCharges 因为和 tenure 强相关分布几乎是复制。想看更精细的趋势可以用 KDE 密度图叠两组曲线注意新版本 seaborn 里 shadeTrue 已改为 fillTrue老脚本直接跑会报参数错误。sns.kdeplot(df_raw.loc[df_raw[Churn] 1, tenure], labelchurn, fillTrue) sns.kdeplot(df_raw.loc[df_raw[Churn] 0, tenure], labelnot churn, fillTrue) plt.legend() plt.show()密度图上通常能看到流失组在 0 到 20 个月区间有一个明显峰值这对应一年内合约到期不再续费的人群。这条信息直接给后面的建模指了方向tenure 一定是树模型最重要的切分变量之一。3.2 类别特征堆叠图Contract 与 PaymentMethod 的流失占比数值看完看类别。对合约类型、付款方式、互联网服务这几个核心字段用 pandas 的 crosstab 做归一化堆叠柱状图直接读出每个类别内部的流失比例。这张图是给业务方汇报时最有说服力的产物。pd.crosstab(df_raw[Contract], df_raw[Churn], normalizeindex).plot( kindbar, stackedTrue, colormapRdBu) plt.title(流失比例 by Contract) plt.ylabel(占比) plt.show()堆叠图的结论通常稳定到让人吃惊按月合约的客户流失比例接近 50%按年合约只有 5% 左右付款方式为电子支票的客户流失率远高于信用卡和自动扣款光纤上网用户的流失率高于 DSL。看到这些规律后建模方向就明确了——模型大概率会在 Contract 上做最主要的切分。这里必须提醒一句堆叠图只说明相关不能下因果结论。“用电子支票导致流失”在业务上站不住脚更可能是年轻用户群体的偏好之一汇报时用词要小心。3.3 相关性热力图验证手动特征工程的方向最后画相关矩阵。因为 one-hot 展开后列很多全画出来是一片密密麻麻的红蓝格子没有意义。我只看 Churn 与各特征的相关系数绝对值前 10 名再针对这些列画热力图。还可以顺带训练一个临时随机森林把 feature_importances_ 排名拿回来和热力图对照。corr_with_churn df.corr(numeric_onlyTrue)[Churn].abs().sort_values(ascendingFalse) print(corr_with_churn.head(10)) sns.heatmap(df[corr_with_churn.head(8).index].corr(numeric_onlyTrue), annotTrue, fmt.2f, cmapRdBu)相关系数排名靠前的通常是 Contract_One year、tenure、service_count 这些。这一步的核心价值不在于复述前面已经发现的规律而在于验证手动特征工程没白做service_count 和 Churn 的相关系数如果比单个服务项都高说明合并方向正确。到了这里特征可视化已经完成使命——用图表证明哪些特征值得进入模型哪些可以忽略并且给后续调参提供了先验优先让树模型在 tenure、Contract、MonthlyCharges 附近做深层次切分。4. 建模与调参从逻辑回归基线到随机森林网格搜索4.1 划分数据集stratifyy 让训练和测试的流失比例一致建模之前先划分数据集顺序不能反。先用 train_test_split 分出训练集和测试集测试集占 20%并且加 stratifyy让测试集里的流失比例和全量数据保持一致。如果不加某次随机划分可能让测试集里流失样本特别少评估分数剧烈波动——这也是“模型预测每次结果不一样”的常见来源之一不是模型问题是划分问题。from sklearn.model_selection import train_test_split X df.drop(columns[Churn]) y df[Churn] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 )random_state42 保证每次运行拿到的训练集、测试集完全一样调参结果才能互相比较。如果去掉它跑两次同样的代码指标不同你会浪费大量时间在怀疑模型上。另一种更严格的做法是分层 K 折交叉验证代替单次划分每个折都保留流失比例分数更稳定代价是训练时间变成 K 倍。实际项目里数据量小或者类别不平衡明显时优先考虑 StratifiedKFold。4.2 逻辑回归基线和随机森林两个最小可运行模型先跑逻辑回归作为基线。逻辑回归必须做标准化因为它的损失函数对特征尺度敏感tenure 这种几十上百的量级和 one-hot 的 0/1 列混在一起不标准化会让数值大的特征主导梯度。随机森林是树模型不需要标准化特征尺度对它无影响。from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, roc_auc_score scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) lr LogisticRegression(max_iter1000, class_weightbalanced, random_state42) lr.fit(X_train_scaled, y_train) y_pred_lr lr.predict(X_test_scaled) print(classification_report(y_test, y_pred_lr)) print(AUC:, roc_auc_score(y_test, lr.predict_proba(X_test_scaled)[:, 1]))max_iter1000 是把默认的 100 调大不然训练时大概率看到 ConvergenceWarning——稀疏特征加上几千行样本100 轮迭代常常不够收敛。class_weightbalanced 让模型自动给少数类加权不会偏向多数类。逻辑回归的定位是底线分数后面所有模型都拿它做对比如果随机森林的 AUC 没有明显优势说明数据关系接近线性不必上复杂模型。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth8, class_weightbalanced, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) y_prob_rf rf.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred_rf)) print(AUC:, roc_auc_score(y_test, y_prob_rf))随机森林参数里n_estimators200 是精度和耗时的折中超过 300 收益很小max_depth8 限制树深防止在 7000 行的小样本上过深拟合class_weightbalanced 再次出现因为树模型不加权叶子节点会偏向多数类。实践里随机森林的 AUC 通常比逻辑回归高 0.05 左右大约在 0.84 上下。如果差距太大回头查清洗和编码如果差距小于 0.02建议直接用逻辑回归可解释性更好业务侧也更容易接受。4.3 用 GridSearchCV 找最优参数scoring 选 roc_auc 而不是 accuracy手工调参到够用即可再进一步可以用网格搜索。GridSearchCV 的 scoring 参数强烈建议选 roc_auc不要用默认 accuracy原因在前面已经讲透准确率会被多数类带偏。搜索范围不要铺太大否则组合数爆炸一跑就是半小时。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], max_depth: [6, 8, 10], min_samples_leaf: [2, 4] } grid GridSearchCV( RandomForestClassifier(class_weightbalanced, random_state42), param_grid, cv5, scoringroc_auc, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)cv5 表示五折交叉验证每折用 4/5 训练、1/5 验证n_jobs-1 表示用满所有 CPU 核心。跑完之后用 grid.best_estimator_ 在测试集上重新评估一遍不要直接拿网格搜索期间的 best_score_ 当最终指标那是交叉验证的内部分数不是测试集分数。如果时间紧可以把 GridSearchCV 换成 RandomizedSearchCV在参数空间随机采样固定次数通常能更快逼近最优组合。5. 避坑流失预测项目里最常翻车的四个地方5.1 现象准确率 74%跟“全猜不流失”一摸一样模型却显示 90%有同学在 Jupyter 里看到准确率 0.90觉得模型可以上线了再看混淆矩阵才发现模型把所有客户都预测成不流失。原因很简单sklearn 默认的准确率是全体预测正确的比例多数类占 74% 时准确率天然就高。解决方法是看三个东西混淆矩阵、召回率、ROC-AUC。流失预测的业务目标是把“正在流失的客户”找出来比把不流失的判对更重要所以重点关注流失类的召回率。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_test, y_pred_rf) print(cm)对照矩阵左下角那一格它的分母就是真实流失客户总数。加了 class_weightbalanced 之后召回率通常能从 0.5 提到 0.8哪怕准确率掉了几个点模型也更有业务价值因为召回名单能覆盖更多真正要走的客户。5.2 现象测试集拟合得比训练集还好标准化泄漏了有人把 StandardScaler 写成 scaler.fit_transform(X_test)然后发现测试集分数异常高开始怀疑模型作弊。这不是模型强是信息泄漏测试集的均值和标准差被用来标准化测试数据等于模型偷看了测试集的整体分布。解决方法是永远只对训练集 fit之后对训练集和测试集都用同一个 scaler 的 transform。交叉验证里同样要注意每个折的 scaler 都只 fit 训练折而不是在全量数据上先 fit 一次。这是初级分析师最容易踩的坑面试也常考。5.3 现象get_dummies 之后模型效果反而变差还多出一堆全 0 列原因多半出在“No internet service”这类取值上。StreamingTV 字段里客户没开通网络是“No internet service”开通了但不看是“No”直接 get_dummies 会拆出两个含义几乎相同的列特征空间被稀释。解决方法是编码前统一语义把“No internet service”替换成“No”。另外某个类别取值如果出现次数很少比如 PaymentMethod 里某种小众方式只有几十个样本one-hot 之后就是一根极稀疏的列逻辑回归的权重会被它带偏树模型影响相对小。处理办法是先用 value_counts 查看分布把频次过低的取值合并到“其他”或者直接用 effect coding。这个检查放清洗阶段做代价最低。5.4 现象同样的代码跑两次预测结果不一样这个现象在随机森林、XGBoost 这类依赖随机性的模型上最常见。有人把原因归结为玄学其实就是没固定随机种子或者数据划分时没加 stratify。解决方法是三层检查train_test_split 写死 random_state模型构造参数里写死 random_stateGridSearchCV 里也写死 random_state。三处都固定结果就能稳定复现。还有一层更隐蔽的波动来自并行计算n_jobs 多进程下运算顺序的微小差异会导致 0.001 级别的分数浮动这个不用纠结以固定 seed 后跑出来的结果为准。6. 用训练好的模型批量预测新客户从概率到落库前面调好的模型终究要拿来用。最后一件事是写一个能接收“未知客户”信息并输出流失概率的函数再用 SHAP 解释单条预测结果让模型不再是黑匣子。先做第一步def predict_churn(one_customer: dict, model, feature_names): df_one pd.DataFrame([one_customer]) df_one pd.get_dummies(df_one) df_one df_one.reindex(columnsfeature_names, fill_value0) prob model.predict_proba(df_one)[0, 1] return prob sample_customer { Contract: Month-to-month, tenure: 8, MonthlyCharges: 79.5, PaymentMethod: Electronic check, service_count: 2 } prob predict_churn(sample_customer, grid.best_estimator_, X_train.columns) print(f流失概率: {prob:.2%})这里的核心是 reindex新客户的字典经过 get_dummies 后列集合和训练集不可能完全一致必须用训练集的 feature_names 重新对齐缺失列补 0、多余列删掉。很多线上预测翻车就翻在这一行少了它模型直接报维度错误或者更隐蔽地输出错误概率。函数返回 predict_proba 输出中索引为 1 的概率业务侧可以按 0.5 阈值分名单更合理的做法是按概率降序取 Top N 做定向挽回。解释单条预测用 SHAP。安装 shap 库后树模型用 TreeExplainer不需要额外背景数据速度也快。不同 shap 版本的返回值格式有差异老版本 shap_values 是两个数组的 list新版本是单个二维数组按列取就行。import shap explainer shap.TreeExplainer(grid.best_estimator_) shap_values explainer.shap_values(X_test[:100]) shap.summary_plot(shap_values[1] if isinstance(shap_values, list) else shap_values, X_test[:100])summary_plot 输出的图上Contract_Month-to-month 和低 tenure 通常排在贡献最前列和可视化阶段的结论互相印证。到这里从数据清洗、特征可视化、模型预测到结果解释的整条链路已经闭环。我自己的习惯是每次跑完流失模型都把最重要的 10 个特征和 SHAP 图存档下次换数据或换模型时先对照旧结果能省掉大量重复探索的时间。希望这套流程对你有所帮助做出一份能汇报、能落地的电信客户流失预测项目。本文还有配套的精品资源点击获取
返回列表