资讯动态

基于Python的电信客户流失预测:从数据清洗到SHAP可解释性分析

发布时间:2026/10/14 19:46:36 来源:尧图企业网站定制
简介这份资源是面向高校学生与数据挖掘初学者的电信客户流失预测与分析完整项目源码可作为数据挖掘课程期末大作业、课程设计或自学练手使用。项目基于Python实现围绕电信用户流失这一典型分类场景涵盖数据预处理、特征降维、模型构建与训练等环节代码配有注释新手也能看懂并快速部署运行。压缩包共36个文件以20个pt模型权重文件、10个py源码文件为主另含4个xml配置、1个gitignore与1个iml工程文件整体约1.79MB目录中划分了数据处理、模型定义、训练脚本与工具函数等模块结构清晰便于按需查阅。目前已有1471人学习下载适合需要完整赛题方案、可复用训练流程与排错参考的读者帮助快速理解电信流失预测的建模思路与工程组织方式。1. 电信客户流失预测一份能跑通的大作业到底长什么样电信客户流失预测这个题目几乎每年数据挖掘课设都会被翻出来做一遍。原因很直接数据集干净、业务解释性强、模型效果容易做出来而且从数据清洗到特征工程再到模型评估整条链路都能覆盖。但真正动手做过的人都知道翻车往往不在模型选型上而在几个看起来不起眼的地方——总费用字段为什么和月费乘 tenure 对不上、Churn 标签怎么转成 0/1、类别不平衡到底要不要处理。这份基于 Python 的电信客户流失预测与分析源码核心目标就是把这些坑提前填好让拿到手的人能直接跑通、看懂、改得动。适合正在做大作业的学生也适合想拿一个完整分类项目练手的数据分析入门者。2. 数据加载与清洗先把黑匣子拆开看2.1 数据集字段含义与加载方式电信客户流失数据集通常包含以下字段客户 ID、性别、是否老年人、是否有伴侣、是否有亲属、在网时长tenure、是否开通电话服务、是否开通多条线路、是否开通互联网服务、互联网服务类型、是否开通在线安全、是否开通在线备份、是否开通设备保护、是否开通技术支持、是否开通流媒体电视、是否开通流媒体电影、合同类型、是否开通无纸化账单、付款方式、月费用、总费用、流失标签。加载时最常见的做法是用 pandas 直接读 CSVimport pandas as pd import numpy as np # 加载数据集注意检查编码部分版本可能是 utf-8 或 latin-1 df pd.read_csv(telco_churn.csv) # 快速查看数据规模和字段类型 print(df.shape) print(df.dtypes) print(df.head()) # 检查缺失值分布 print(df.isnull().sum())逻辑说明先看 shape 确认样本量和字段数再看 dtypes 区分数值型和类别型字段最后检查缺失值。参数说明pd.read_csv的encoding参数根据实际文件调整如果报 UnicodeDecodeError 就换latin-1或gbk。df.isnull().sum()返回每个字段的缺失数量TotalCharges 字段经常是 object 类型且带空格需要额外处理。2.2 TotalCharges 的类型转换与异常值处理TotalCharges 这个字段是新手最容易踩坑的地方。它表面上是数值但读进来往往是 object 类型因为里面混了空格字符串。直接转 float 会报错必须先替换再转换# 将空格替换为 NaN再强制转为数值类型 df[TotalCharges] df[TotalCharges].replace( , np.nan) df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) # 查看转换后缺失情况 print(df[TotalCharges].isnull().sum()) # 对于缺失的 TotalCharges用 tenure * MonthlyCharges 填充 mask df[TotalCharges].isnull() df.loc[mask, TotalCharges] df.loc[mask, tenure] * df.loc[mask, MonthlyCharges] # 再次确认无缺失 print(df[TotalCharges].isnull().sum())逻辑说明replace( , np.nan)把空格变成标准缺失值pd.to_numeric配合errorscoerce把无法转换的值也变成 NaN。填充策略选择 tenure 乘月费是因为总费用本质上就是历史月费累加这个近似在业务上站得住。参数说明errorscoerce表示无法转换时赋 NaN 而不是报错这是处理脏数据时的安全做法。2.3 标签编码与类别字段处理Churn 字段是 Yes/No需要转成 1/0。其他类别字段也要做编码但不要一上来就 one-hot先看基数# 标签编码 df[Churn] df[Churn].map({Yes: 1, No: 0}) # 查看类别字段的取值数量 cat_cols df.select_dtypes(include[object]).columns.tolist() for col in cat_cols: print(col, df[col].nunique()) # 对二值类别字段做 map 编码多值字段留给后续 one-hot binary_map {Yes: 1, No: 0, Male: 1, Female: 0} for col in [gender, Partner, Dependents, PhoneService, PaperlessBilling]: df[col] df[col].map(binary_map)逻辑说明先区分二值和多值类别字段。二值字段直接 map 成 0/1多值字段如 InternetService、Contract、PaymentMethod保留到特征工程阶段做 one-hot。参数说明select_dtypes(include[object])筛选出所有字符串字段nunique()看每个字段有多少个不同取值取值超过 2 的就不适合直接二值映射。3. 特征工程把原始字段变成模型能吃的信号3.1 数值特征的分箱与标准化tenure、MonthlyCharges、TotalCharges 是三个核心数值字段。tenure 的分布往往不是线性的新客户流失率高老客户稳定所以分箱比直接放原始值更有效# tenure 分箱按业务含义切成新客户、成长期、稳定期、忠诚期 df[tenure_bin] pd.cut( df[tenure], bins[0, 12, 24, 48, 72], labels[新客户, 成长期, 稳定期, 忠诚期] ) # 对数值字段做标准化方便后续逻辑回归等对尺度敏感的模型 from sklearn.preprocessing import StandardScaler scaler StandardScaler() num_cols [tenure, MonthlyCharges, TotalCharges] df[num_cols] scaler.fit_transform(df[num_cols])逻辑说明分箱的依据是电信业务里常见的客户生命周期划分12 个月以内算新客户12 到 24 个月是成长期24 到 48 个月是稳定期超过 48 个月基本算忠诚客户。标准化用 StandardScaler 做 z-score 变换让均值为 0、方差为 1。参数说明bins的边界根据实际数据分布调整如果 tenure 最大值不到 72最后一个边界改成实际最大值。fit_transform只在训练集上做测试集要用transform否则会数据泄露。3.2 多值类别字段的 one-hot 编码InternetService、Contract、PaymentMethod 这些字段有多个取值直接用 LabelEncoder 会引入虚假的大小关系正确做法是 one-hot# 对多值类别字段做 one-hot multi_cat_cols [InternetService, Contract, PaymentMethod, tenure_bin] df pd.get_dummies(df, columnsmulti_cat_cols, drop_firstTrue) # 删除无用的客户 ID 字段 df df.drop(columns[customerID]) # 确认最终特征矩阵 print(df.shape) print(df.columns.tolist())逻辑说明drop_firstTrue是为了避免虚拟变量陷阱即多个 one-hot 列之间存在完全共线性。删除 customerID 是因为它只是标识符对预测没有信息量。参数说明columns指定需要编码的字段列表drop_first在树模型里可以设 False但在逻辑回归里必须设 True。3.3 构造衍生特征月均费用与合同类型交互原始字段之外可以构造几个有业务含义的衍生特征。比如月均费用TotalCharges 除以 tenure能反映客户的消费水平合同类型和 tenure 的交互能捕捉不同合同下的留存差异# 月均费用注意此时 TotalCharges 和 tenure 已被标准化需用原始值计算 # 建议在标准化之前先构造好衍生特征 df[avg_monthly] df[TotalCharges] / (df[tenure] 1) # 合同类型与在网时长的交互 df[contract_tenure] df[tenure] * (df[Contract_One year] 2 * df[Contract_Two year]) # 检查新特征与标签的相关性 print(df[[avg_monthly, contract_tenure, Churn]].corr()[Churn])逻辑说明avg_monthly用 TotalCharges 除以 tenure 加 1加 1 是防止 tenure 为 0 时除零。contract_tenure把合同类型转成数值权重后与 tenure 相乘月结合同权重为 0一年合同为 1两年合同为 2。参数说明衍生特征最好在标准化之前构造否则标准化后的值相除没有业务意义。相关系数只作为参考绝对值低于 0.05 的特征可以考虑剔除。4. 模型训练与评估从逻辑回归到集成模型4.1 训练集测试集划分与类别不平衡处理电信流失数据里流失样本通常只占 20% 到 30%存在类别不平衡。划分数据时要分层抽样保证训练集和测试集的流失比例一致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, random_state42, stratifyy ) print(训练集流失比例:, y_train.mean()) print(测试集流失比例:, y_test.mean())逻辑说明stratifyy保证划分后正负样本比例与原始数据一致避免测试集里流失样本过少导致评估不稳定。参数说明test_size0.2是常见划分比例数据量小的时候可以调到 0.3。random_state固定随机种子保证结果可复现。4.2 逻辑回归基线模型与系数解读先跑一个逻辑回归做基线好处是系数可以直接解释能看出哪些特征对流失影响最大from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, roc_auc_score lr LogisticRegression(max_iter1000, class_weightbalanced, random_state42) lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) y_prob_lr lr.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred_lr)) print(AUC:, roc_auc_score(y_test, y_prob_lr)) # 查看系数最大的前十个特征 coef_df pd.DataFrame({feature: X_train.columns, coef: lr.coef_[0]}) print(coef_df.reindex(coef_df[coef].abs().sort_values(ascendingFalse).index).head(10))逻辑说明class_weightbalanced让模型自动调整权重缓解类别不平衡。max_iter1000防止迭代次数不够导致不收敛。AUC 比准确率更适合不平衡数据因为它衡量的是排序能力而不是阈值切分。参数说明predict_proba返回每个样本属于各类的概率取第二列是流失概率。系数为正表示该特征增加流失风险为负表示降低风险。4.3 随机森林与特征重要性逻辑回归之后用随机森林看非线性关系同时输出特征重要性from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf5, class_weightbalanced, random_state42, n_jobs-1 ) 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)) # 特征重要性排序 importance pd.DataFrame({ feature: X_train.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(10))逻辑说明n_estimators200是树的数量越多越稳定但计算越慢。max_depth10限制树深防止过拟合。min_samples_leaf5保证叶子节点至少有 5 个样本避免模型记住噪声。n_jobs-1用满所有 CPU 核心加速训练。参数说明特征重要性是基尼不纯度减少量的平均数值越大表示该特征对分类贡献越大。4.4 模型对比与阈值调整两个模型跑完后用统一指标对比并根据业务需求调整分类阈值from sklearn.metrics import roc_curve # 对比 AUC print(LR AUC:, roc_auc_score(y_test, y_prob_lr)) print(RF AUC:, roc_auc_score(y_test, y_prob_rf)) # 找最佳阈值约登指数最大化 fpr, tpr, thresholds roc_curve(y_test, y_prob_rf) youden tpr - fpr best_threshold thresholds[youden.argmax()] print(最佳阈值:, best_threshold) # 用最佳阈值重新分类 y_pred_best (y_prob_rf best_threshold).astype(int) print(classification_report(y_test, y_pred_best))逻辑说明约登指数是敏感度加特异度减 1最大化约登指数等价于在 ROC 曲线上找离随机线最远的点。默认阈值 0.5 在不平衡数据里往往不是最优的。参数说明roc_curve返回假正率、真正率和对应阈值argmax()找约登指数最大时的索引。阈值调完后要重新看分类报告确认精确率和召回率的平衡是否符合业务预期。5. 避坑与排查那些让大作业翻车的细节5.1 数据泄露标准化在划分之前做了现象模型在测试集上 AUC 高得离谱超过 0.95但换一批数据就崩。原因标准化或 one-hot 编码在 train_test_split 之前对整个数据集做了测试集的信息泄露到了训练过程。解决先划分训练集和测试集所有拟合操作scaler.fit、编码器.fit只在训练集上做测试集用 transform。5.2 TotalCharges 缺失值直接 drop 导致样本量骤减现象清洗后发现样本少了几百条模型训练不稳定。原因TotalCharges 里的空格被转成 NaN 后直接 dropna但缺失的往往是 tenure 为 0 的新客户这批人恰恰是流失预测的重点人群。解决用 tenure 乘 MonthlyCharges 填充或者单独把 tenure 为 0 的样本拿出来分析不要一刀切删掉。5.3 one-hot 之后特征维度爆炸现象做完 get_dummies 后特征从 20 个变成 50 多个逻辑回归跑得很慢且系数不稳定。原因对高基数类别字段如 PaymentMethod 有 4 个取值做 one-hot 本身没问题但如果数据里还有类似 customerID 这种唯一值字段没删就会炸出几千列。解决编码前先删掉 ID 类字段对基数超过 10 的类别字段考虑用目标编码或频率编码替代 one-hot。5.4 用准确率评估不平衡数据现象模型准确率 80%看起来不错但流失客户一个都没预测出来。原因如果流失样本只占 20%全部预测为不流失就有 80% 准确率。解决看混淆矩阵、精确率、召回率和 AUC不要只看准确率。业务上如果更关心抓住流失客户就重点看召回率。5.5 随机森林特征重要性直接当因果解释现象特征重要性显示 MonthlyCharges 最重要就得出结论“月费高导致流失”。原因特征重要性只反映关联强度不反映因果关系而且高基数特征和连续特征在树模型里天然容易获得高重要性。解决结合逻辑回归系数、业务逻辑和分组统计一起看不要单凭特征重要性下结论。6. 进阶技巧用 SHAP 把模型预测讲清楚大作业如果只停在模型对比分数不会低但也不会高。真正能拉开差距的是可解释性分析。SHAP 是目前最实用的工具它能告诉你每个样本的预测结果里每个特征贡献了多少。import shap # 用训练好的随机森林模型做 SHAP 分析 explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_test) # 全局特征重要性图 shap.summary_plot(shap_values[1], X_test, plot_typebar) # 单个样本的解释 shap.force_plot( explainer.expected_value[1], shap_values[1][0], X_test.iloc[0], matplotlibTrue )逻辑说明TreeExplainer是专门给树模型用的计算速度快。shap_values[1]取的是流失类别的 SHAP 值。summary_plot的 bar 类型展示全局重要性beeswarm 类型展示每个特征的影响方向。force_plot展示单个样本的预测分解红色推高流失概率蓝色拉低。参数说明explainer.expected_value是模型在训练集上的平均预测值作为基准线。实际用的时候我一般会挑几个典型样本做 force_plot一个高概率流失的、一个低概率流失的、一个模型判错的。把这三个图的解释写进报告比堆一堆模型指标有说服力得多。另外 SHAP 值可以按特征聚合后和业务字段做交叉比如看看合同类型为月付的客户里哪些特征在推高流失风险这样分析就落到了业务动作上。有个习惯我保持了几年每次做完一个分类项目都会把 SHAP 图和混淆矩阵放在同一页对比看。混淆矩阵告诉你错在哪SHAP 告诉你为什么错。两个结合起来模型就不再是黑匣子而是一个能对话的分析对象。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑