
简介这份资源是面向高校学生与数据挖掘初学者的电信客户流失预测与分析完整项目源码可作为数据挖掘课程期末大作业或课程设计使用。项目基于Python实现围绕电信用户流失这一典型分类场景涵盖数据预处理、特征降维、模型构建与训练等环节代码附有注释新手也能理解整体流程。压缩包共36个文件以20个pt模型权重文件、10个py源码文件为主另含4个xml配置、1个gitignore与1个iml工程文件整体约1.79MB目录中可见数据处理、模型定义、训练脚本与工具函数等模块结构清晰便于按功能查阅。目前已有1471人学习下载说明该案例在同类作业中具有较高参考价值。下载后简单部署即可运行读者可据此掌握从数据清洗到模型训练评估的完整思路并在此基础上调整参数或替换数据集快速完成一份功能完善、逻辑完整的分析项目。1. 电信客户流失预测一份数据挖掘大作业为什么值得你花两周做透电信客户流失预测这个题目几乎每年都会出现在数据挖掘、机器学习课程的期末大作业清单里。表面上看它只是一个二分类任务给你一份客户表预测谁会走。但真正动手做过的人都知道这份数据集的价值远不止交作业——它同时覆盖了缺失值处理、类别不平衡、特征工程、多模型对比、业务解释这几条主线是少数能在一份数据里把数据挖掘全流程跑通的场景。对新手来说它能让你第一次完整经历「读数据到出结论」的闭环对熟手来说它是一块测试特征构造思路和评估策略的试验田。这篇笔记就围绕基于 Python 的电信客户流失预测与分析源码该怎么写、参数怎么调、坑在哪把一份能拿得出手的大作业拆开讲清楚。2. 先搞清楚预测目标与数据底盘字段含义决定特征工程上限2.1 流失标签怎么定义为什么它决定了整个项目的走向拿到数据的第一步不是急着read_csv而是先确认标签列。电信流失数据集里通常有一列叫Churn取值是Yes/No它表示客户在观察窗口内是否主动终止了服务。这个定义看起来简单但它隐含了两个关键约束第一标签是历史快照不是实时状态所以你做的模型本质上是在拟合「过去某段时间内流失的人长什么样」第二流失是低频事件正样本占比往往只有两成左右这直接决定了后面不能用准确率当主指标。我一般会先做三件事看标签分布、看字段类型、看缺失情况。这三件事做完整个项目的难度和方向基本就定了。如果正样本低于 15%你就要认真考虑类别权重或者重采样如果字段里既有数值又有类别那编码方式的选择会直接影响树模型和线性模型的差距。import pandas as pd df pd.read_csv(telco_churn.csv) # 标签分布判断类别是否失衡 print(df[Churn].value_counts(normalizeTrue)) # 字段类型概览数值 vs 类别 print(df.dtypes.value_counts()) # 缺失情况哪些列需要单独处理 missing df.isnull().sum() print(missing[missing 0].sort_values(ascendingFalse))这段代码的作用是快速建立数据画像。value_counts(normalizeTrue)给出的是比例而不是绝对数量方便你判断失衡程度dtypes.value_counts()让你一眼看出类别字段有多少因为类别字段多意味着编码和特征交叉的空间大缺失值排序则是为了决定哪些列直接删、哪些列要填。参数上没有什么需要调的但要注意如果Churn列是Yes/No字符串后面建模前必须映射成 0/1否则 sklearn 会直接报错。2.2 三类字段的处理策略数值、类别、以及容易被忽略的 ID 列电信流失数据的字段大致可以分成三组。第一组是人口属性比如性别、是否老年人、是否有伴侣第二组是账户信息比如合同类型、付款方式、月费第三组是服务订阅情况比如是否开通了网络安全、在线备份、流媒体电视。这三组字段的处理方式完全不同。数值字段里tenure在网时长和MonthlyCharges月费是最重要的两个。tenure的分布往往双峰新客户和老客户各占一堆直接标准化效果一般我通常会额外构造一个「是否新客户」的布尔特征。类别字段里Contract合同类型是流失预测里最强的单特征之一按月付费的客户流失率远高于两年合同客户这个字段一定要保留并且做有序编码或者独热编码。最容易被忽略的是customerID这类列。它看起来是唯一标识实际上没有任何预测价值留着只会让模型学到噪声。我的习惯是在特征工程开始前就把它单独存一份映射表然后从特征矩阵里删掉这样后面排查问题时还能靠 ID 回溯到原始记录。# 删除无预测价值的 ID 列但保留一份备份 id_backup df[customerID].copy() df df.drop(columns[customerID]) # 标签映射 df[Churn] df[Churn].map({Yes: 1, No: 0}) # 构造新客户特征在网时长小于 12 个月视为新客户 df[is_new_customer] (df[tenure] 12).astype(int) # 合同类型有序编码按月 一年 两年 contract_order {Month-to-month: 0, One year: 1, Two year: 2} df[Contract_encoded] df[Contract].map(contract_order)这里的关键逻辑是is_new_customer这个特征不是拍脑袋加的它来自tenure分布的双峰观察Contract_encoded用有序编码而不是独热是因为合同类型本身有强弱顺序有序编码能让树模型更高效地找到切分点。参数上12 个月这个阈值可以根据你的数据分布调整如果新客户定义是 6 个月就改成 6。注意map之后如果出现NaN说明原始字段里有你没预料到的取值这时候要回去检查数据而不是直接fillna。3. 从原始表到模型输入特征工程与不平衡处理的实操路径3.1 数值标准化与类别编码什么时候用 StandardScaler什么时候用 OneHotEncoder特征工程这一步最常见的翻车点是把所有字段一股脑塞进ColumnTransformer然后发现模型效果还不如不处理。问题往往出在编码方式选错了。数值字段里MonthlyCharges和TotalCharges量纲接近标准化后对逻辑回归友好但tenure这种双峰分布标准化并不会让它更接近正态反而可能让树模型多切几刀。我的做法是树模型随机森林、XGBoost不做标准化直接喂原始数值线性模型逻辑回归才做标准化。类别字段的编码更讲究。低基数类别比如性别只有两个值用独热编码没问题高基数类别比如付款方式可能有四种以上如果独热维度会膨胀这时候要么用目标编码要么直接交给树模型处理。我一般会先跑一版独热编码的逻辑回归作为基线再跑一版不做编码的随机森林对比两者的 AUC差距大的话就说明编码方式对当前数据影响很大。from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.pipeline import Pipeline numeric_features [tenure, MonthlyCharges, TotalCharges] categorical_features [gender, Partner, Dependents, PaymentMethod] # 数值标准化 类别独热注意 handle_unknown 防止线上新类别报错 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), numeric_features), (cat, OneHotEncoder(handle_unknownignore), categorical_features), ] ) pipeline Pipeline([ (prep, preprocessor), (clf, None), # 模型占位后面替换 ])这段代码的核心是ColumnTransformer把不同处理逻辑并行化避免手动拼接特征矩阵时列顺序错乱。handle_unknownignore是一个容易被忽略但非常重要的参数如果训练集里没有某个类别值测试集出现了不加这个参数会直接抛异常。参数上StandardScaler默认按列减均值除标准差如果你的数据有极端离群值可以考虑换成RobustScaler。3.2 类别不平衡的三种处理方式权重、采样与阈值移动电信流失数据的正样本比例通常在 20% 到 27% 之间这个失衡程度不算极端但足以让准确率变成一个误导性指标。我见过太多大作业用准确率 80% 来证明模型好实际上把所有样本都预测成「不流失」也能拿到 73% 以上的准确率。正确的做法是看 AUC 和召回率。处理不平衡有三种常见方式。第一种是设置class_weightbalanced让模型在训练时自动给少数类更高权重这是最省事的方式逻辑回归和随机森林都支持。第二种是重采样比如 SMOTE 合成少数类样本但要注意 SMOTE 只能在训练集上做测试集绝对不能碰否则评估结果会虚高。第三种是阈值移动模型输出概率后不按 0.5 切而是根据业务需求选一个更低的阈值来提高召回。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, recall_score 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 ) # 方式一类别权重 lr LogisticRegression(class_weightbalanced, max_iter1000) rf RandomForestClassifier(class_weightbalanced, n_estimators200, random_state42) for name, model in [(LR, lr), (RF, rf)]: pipe Pipeline([(prep, preprocessor), (clf, model)]) pipe.fit(X_train, y_train) proba pipe.predict_proba(X_test)[:, 1] print(name, AUC:, roc_auc_score(y_test, proba))stratifyy保证训练集和测试集的标签比例一致这是做不平衡数据时必须加的参数。max_iter1000是因为逻辑回归在标准化后的数据上有时需要更多迭代才能收敛默认 100 会报收敛警告。n_estimators200是随机森林的树数量太少方差大太多训练慢200 是一个比较稳的起点。跑完这段代码后如果两个模型的 AUC 都在 0.82 以上说明特征工程基本到位如果低于 0.75要回去检查是不是漏了关键字段或者编码方式有问题。4. 模型训练与评估AUC、召回率和业务阈值的三角权衡4.1 为什么准确率在这个任务里几乎没用前面提到过流失预测的正样本比例低准确率会被多数类主导。举个具体数字如果测试集有 1000 个客户其中 250 个流失你把所有客户都预测成不流失准确率是 75%但召回率是 0。这个模型在业务上毫无价值因为一个流失客户都没抓到。所以从大作业的角度评估指标至少要报 AUC、召回率和 F1准确率可以提但不要当主指标。AUC 衡量的是模型把正样本排在负样本前面的能力它对阈值不敏感适合用来比较不同模型的排序质量。召回率衡量的是「真正流失的人里你抓到了多少」这个指标直接对应业务价值。F1 是精确率和召回的调和平均适合在两者之间找平衡。我一般会先看 AUC 选模型再看召回率定阈值。from sklearn.metrics import classification_report, confusion_matrix # 用随机森林做最终模型输出多指标报告 final_pipe Pipeline([(prep, preprocessor), (clf, rf)]) final_pipe.fit(X_train, y_train) proba final_pipe.predict_proba(X_test)[:, 1] pred_default (proba 0.5).astype(int) pred_low (proba 0.3).astype(int) print(默认阈值 0.5:) print(classification_report(y_test, pred_default)) print(降低阈值到 0.3:) print(classification_report(y_test, pred_low))这段代码对比了两个阈值下的分类报告。你会看到阈值从 0.5 降到 0.3 后召回率明显上升精确率下降。这不是模型变好了而是你主动选择了「宁可错杀不可放过」的策略。参数上0.3 不是固定值你可以画一条 precision-recall 曲线找到召回率满足业务要求时精确率最高的那个点。注意predict_proba返回的是两列第一列是负类概率第二列才是正类概率取[:, 1]不要取错。4.2 交叉验证与超参数搜索GridSearchCV 的正确打开方式单次 train_test_split 的结果有随机性换一个random_state可能 AUC 波动两三个点。要让大作业的结果更可信至少要做 5 折交叉验证。如果时间允许用GridSearchCV在训练集上搜一轮超参数再把最优参数放到测试集上评估。注意搜索空间不要设太大随机森林的n_estimators和max_depth是两个最值得调的参数逻辑回归的C是正则化强度的倒数越小正则越强。from sklearn.model_selection import GridSearchCV param_grid { clf__n_estimators: [100, 200, 300], clf__max_depth: [None, 10, 20], clf__min_samples_leaf: [1, 5, 10], } grid GridSearchCV( Pipeline([(prep, preprocessor), (clf, RandomForestClassifier( class_weightbalanced, random_state42))]), param_grid, cv5, scoringroc_auc, n_jobs-1, ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优 AUC:, grid.best_score_)cv5表示 5 折交叉验证scoringroc_auc指定用 AUC 作为选择标准n_jobs-1表示用满所有 CPU 核心加速。参数网格里max_depthNone表示不限制深度让树自由生长这在小数据集上通常效果不差但容易过拟合所以同时试 10 和 20 做对比。min_samples_leaf控制叶子节点最少样本数增大它可以防止模型学到噪声。跑完 GridSearchCV 后best_score_是交叉验证的平均 AUC通常比单次测试集 AUC 略低这是正常的。5. 避坑与排查电信流失预测里最容易翻车的五个地方5.1 坑一TotalCharges 列混进了空字符串导致类型转换失败现象读数据时TotalCharges显示为 object 类型做数值计算时报TypeError。原因这一列里有些新客户的TotalCharges是空字符串而不是NaNpandas 读进来默认当字符串处理。解决用pd.to_numeric(df[TotalCharges], errorscoerce)强制转换空字符串会变成NaN然后再决定是填 0 还是填中位数。我一般填 0因为新客户还没产生总费用填 0 比填中位数更符合业务逻辑。5.2 坑二SMOTE 用在了测试集上导致 AUC 虚高现象做了 SMOTE 之后 AUC 从 0.84 跳到 0.95开心得太早。原因SMOTE 在测试集上合成了少数类样本测试集分布被改变了评估结果不再反映真实泛化能力。解决SMOTE 只能在训练集上做而且要在交叉验证的每一折内部做不能在全量数据上做完再切分。更稳妥的做法是先用class_weight跑一版基线如果 AUC 已经够用就不折腾采样。5.3 坑三把 customerID 当特征喂进模型现象模型训练不报错但特征重要性里 customerID 排得很高。原因customerID 是唯一标识模型可能学到「ID 小的客户更容易流失」这种毫无意义的规律。解决在特征工程第一步就删掉 ID 列如果后面需要回溯单独存一份映射表。这个坑在新手里非常常见因为 sklearn 不会阻止你喂字符串 ID只会默默做编码然后给你一个看似合理的结果。5.4 坑四用准确率选模型选出一个「全预测不流失」的废物现象某个模型准确率 78%但混淆矩阵里正样本一个都没抓到。原因类别不平衡下多数类主导了准确率。解决把评估指标换成 AUC 和召回率并且在classification_report里确认正类的 recall 不是 0。如果正类 recall 低于 0.5说明模型基本没学到东西要回去检查特征和标签映射。5.5 坑五特征重要性解释和业务逻辑对不上现象随机森林给出的特征重要性里某个无关字段排在前三。原因可能是编码方式让这个字段产生了大量稀疏列树模型在稀疏特征上容易过拟合。解决对高基数类别字段改用目标编码或者直接删除然后重新训练看重要性排序是否合理。另外特征重要性有impurity和permutation两种后者更可靠但计算慢大作业里用前者就够了但要心里有数。6. 让大作业脱颖而出的进阶技巧从模型输出到业务建议6.1 用 SHAP 做单客户解释把预测概率翻译成挽留动作一份大作业如果只停在 AUC 和混淆矩阵很难和别人的拉开差距。真正让老师或面试官眼前一亮的是你能把模型输出翻译成业务语言。SHAP 是目前最实用的解释工具它能告诉你每个特征对单个客户预测结果的贡献方向。比如某个客户流失概率 0.82SHAP 会显示Contract_Month-to-month推高了 0.15tenure低推高了 0.12那挽留动作就很明确优先推长期合同优惠。import shap # 用训练好的随机森林管道做解释 explainer shap.TreeExplainer(final_pipe.named_steps[clf]) X_test_transformed final_pipe.named_steps[prep].transform(X_test) shap_values explainer.shap_values(X_test_transformed) # 输出第一个客户的特征贡献 shap.initjs() shap.force_plot( explainer.expected_value[1], shap_values[1][0], X_test_transformed[0], )这段代码里TreeExplainer专门用于树模型比通用的KernelExplainer快很多。shap_values[1]取的是正类流失的贡献值[0]是第一个测试样本。force_plot会生成一个可视化图红色表示推高流失概率蓝色表示拉低。参数上没有什么需要调的但要注意X_test_transformed必须是经过preprocessor转换后的矩阵不能直接喂原始 DataFrame否则列对不上会报错。6.2 把流失概率分档输出可执行的客户分层名单模型给出的是连续概率业务需要的是可执行的分层。我一般会把测试集客户按预测概率分成三档高风险大于 0.7、中风险0.4 到 0.7、低风险小于 0.4。高风险客户直接进挽留名单中风险客户做定向优惠测试低风险客户不打扰。这样一份分层名单比一个 AUC 数字更有说服力因为它直接对应运营动作。风险档位概率区间建议动作预期召回覆盖高风险 0.7人工外呼 合同优惠覆盖约 60% 流失客户中风险0.4 - 0.7短信推送折扣覆盖约 25% 流失客户低风险 0.4不主动触达覆盖约 15% 流失客户这张表的数字不是固定的要根据你的测试集实际分布来填。做法是先按概率排序取累计召回达到 60% 时的概率截断点作为高风险阈值再取累计召回达到 85% 时的截断点作为中风险阈值。这样分出来的档位是有数据支撑的不是拍脑袋定的。6.3 一个我踩过的坑不要为了刷 AUC 而过度调参最后说一个血泪经验。我刚开始做这个题目的时候花了整整三天调随机森林的超参数把 AUC 从 0.84 刷到了 0.86然后发现换一个random_state重新切分数据AUC 又掉回 0.83。后来才明白在小数据集上两个点的 AUC 差异很可能只是噪声不值得花那么多时间。真正让这份大作业有价值的是特征工程有没有业务逻辑、评估指标有没有选对、结论有没有可执行性。把调参的时间省下来做 SHAP 解释和分层名单收益大得多。希望帮到你。本文还有配套的精品资源点击获取