
简介面向电信客户流失预测的完整数据分析与建模项目含可运行的Python源码、训练好的模型文件.sav及项目说明文档适合计算机、数据科学、人工智能等专业学生作为课程设计或毕业设计参考。资源共15个文件以Python脚本、CSV数据集、模型文件为主配有多张结果图表PNG和两份TXT/Markdown说明压缩包仅545KB轻量易部署。项目覆盖数据预处理、特征可视化、模型对比与未知客户流失概率预测等环节已训练模型可直接用于测试可视化图包括ROC曲线、精确率-召回率曲线、混淆矩阵等便于理解评估指标。已有167人学习下载代码经测试可正常运行既可供初学者完整跟练也可作为项目立项演示或课设素材具有较高借鉴价值。1. 电信客户流失预测这个 Python 项目到底值不值得复现电信公司客户流失分析是典型的二分类任务拿到一批客户档案和账单数据做特征可视化找到流失规律再用模型预测未知客户会不会流失。这个项目真正难的不是建模——逻辑回归和随机森林都能跑出不错的结果——而是数据清洗、特征理解和评估口径。标题里的“python源码项目说明”意味着它是一份可复现的工程包而不是一段演示代码适合三类人想练完整数据链路的新手、需要做课程设计或毕设的学生、以及要跟运营同学对接并输出名单的分析师。接下来按数据清洗、特征可视化、建模拟合、排坑排查的顺序把整套方案讲透。2. 先摸清数据底细客户表怎么读、脏数据藏在哪这类项目的数据集通常是一张客户维度表一行一个客户列包含档案信息、服务订阅、账单金额和流失标签。最常用的字段有客户ID、性别、是否老人、是否已婚、是否有家属、在网时长、电话服务、多线路、互联网服务、在线安全、在线备份、设备保护、技术支持、流媒体电视、流媒体电影、合同类型、付款方式、电子账单、月费用、总费用和流失标签。字段不多但数据类型很乱是 Python 数据分析里最容易翻车的第一步。2.1 字段长什么样先建一张字段对照表拿到数据后不要急着画图先把字段按“档案类、服务类、账单类、标签”分组搞清楚每个字段的类型和取值含义。下面这张表是我做这类项目时固定的第一张表也建议你写进项目说明里字段类型取值角色customerID字符串唯一客户编号不参与建模仅用于最后名单回联gender字符串Male / Female候选特征SeniorCitizen整数0 / 1候选特征注意不是 Yes/NoPartner、Dependents字符串Yes / No候选特征tenure整数0~72 个月候选特征流失分析的核心变量PhoneService字符串Yes / No候选特征InternetService字符串DSL / Fiber optic / No候选特征OnlineSecurity 等附加服务字符串Yes / No / No internet service候选特征Contract字符串Month-to-month / One year / Two year候选特征流失信号最强之一PaymentMethod字符串四种付款方式候选特征MonthlyCharges浮点账单金额候选特征TotalCharges字符串累计金额读进来是 object通常删除详见避坑章Churn字符串Yes / No标签把这张表落到项目说明里最大的好处是过两个月自己回来看或者交接给别人不会对着列名发呆。特别是 SeniorCitizen它本身是 0/1 整数很多人误当成字符串做 one-hot白白多生成一列冗余特征后面建模时特征重要性全乱。2.2 用 pandas 做类型检查TotalCharges 里的空格是最常见的坑类型检查这一步我的经验是只看两个东西dtypes和缺失值统计。很多新手在这里只跑一个info()就略过了结果后面建模时各种报错回头才发现是类型没转对。import pandas as pd # 常见做法是数据集命名为 telco_churn.csv放在项目根目录 data/ 下 df pd.read_csv(data/telco_churn.csv) # 1. 看形状7043 行 21 列是这类数据集的典型规模 print(df.shape) # 2. 看类型重点找“应该是数值却是 object”的列 print(df.dtypes) # 3. 看缺失值TotalCharges 很少直接显示为 NaN print(df.isna().sum())这段代码的逻辑很直接先确认表格规模再逐列检查类型最后看缺失。这里有一个关键坑TotalCharges 从 CSV 读进来通常是 object 类型因为原始数据里把空值写成了空字符串pandas 为了不报错干脆整列当文本处理。# 把 TotalCharges 转成数值遇到空字符串统一变成 NaN df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) # 看看哪些行的 TotalCharges 是空的 print(df[df[TotalCharges].isna()][[customerID, tenure, MonthlyCharges]])errorscoerce的含义是转不了的值不报错直接置为 NaN。这是处理脏数值列的常用手段比pd.to_numeric默认的抛异常行为更适合探索阶段。转完之后你会看到这些空值客户集中在 tenure 很短的群体里这为后面的缺失值处理提供了依据。2.3 缺失值和异常值直接删还是填充要看业务含义TotalCharges 转完数值后会出现少量 NaN常见的处理方式有两种删除整行或者用 0 填充。这个决策要回到业务含义上——TotalCharges 是累计消费金额tenure 很短的客户总费用本来就接近 0空值大概率是系统没来得及生成账单。与其填一个猜测值不如直接删掉这些行避免把“缺失”本身当成一种信号塞进模型。# 删除 TotalCharges 为空的记录并重置索引 df df.dropna(subset[TotalCharges]).reset_index(dropTrue) # 确认没删错剩余样本量和流失占比 print(df.shape) print(df[Churn].value_counts(normalizeTrue))dropna(subset[TotalCharges])只检查这一列不会误删其他字段的缺失值reset_index(dropTrue)是为了让索引连续后面做train_test_split和名单回联时少踩坑。删完后看一下流失占比正常这类数据集流失率在 26% 左右这个数字决定了后面模型评估不能只看准确率。3. 特征可视化四张图把流失原因钉在墙上特征可视化的目的不是把图画得好看而是用图回答三个问题流失客户和非流失客户在哪个特征上差异最大、哪些特征其实高度相关、哪些特征可以直接从模型里丢掉。这个环节用到的是 Python 数据分析与可视化里最常用的 matplotlib 和 seaborn两个库配合能覆盖这类项目的全部画图需求。3.1 单变量分布先看标签再分特征画多变量之前先看标签分布和几个核心特征的边际分布。流失标签分布决定了你要不要做类别不平衡处理合同类型、付款方式这些分类特征的分布则告诉你样本量是否足够支撑分组对比。import matplotlib.pyplot as plt import seaborn as sns # 设置全局画布风格避免每张图重复配置 sns.set_style(whitegrid) fig, axes plt.subplots(2, 2, figsize(14, 10)) # 第一张流失标签确认类别分布 sns.countplot(datadf, xChurn, axaxes[0][0]) axes[0][0].set_title(Churn Distribution) # 第二张合同类型看样本量是否均衡 sns.countplot(datadf, xContract, axaxes[0][1]) axes[0][1].set_title(Contract Type Distribution) # 第三张付款方式通常四种取值分布极不均衡 sns.countplot(datadf, xPaymentMethod, axaxes[1][0]) axes[1][0].tick_params(axisx, rotation45) # 第四张在网时长直方图流失客户集中在低 tenure 段 sns.histplot(datadf, xtenure, bins30, axaxes[1][1]) axes[1][1].set_title(Tenure Distribution) plt.tight_layout() plt.show()countplot适合离散分类特征histplot适合连续特征bins30意思是把 tenure 从 0 到 72 切成 30 个桶太粗会掩盖细节太细会有很多空桶。figsize(14, 10)是四宫格布局的合理尺寸排版紧凑但标签不重叠。这四张图的目的只是建立“数据长什么样”的直觉先不急着下结论。3.2 交叉分析合同类型和 tenure 是流失的照妖镜单变量分布看不出规律真正的信号在交叉分析里。流失分析最值得画的两张图是“合同类型 × 流失比例”和“tenure × 是否流失的分布对比”。前者用百分比堆叠柱状图后者用分组直方图两张图合起来基本能解释这家电信公司一半的流失原因。# 用 crosstab 算合同类型各组内的流失占比 churn_by_contract pd.crosstab(df[Contract], df[Churn], normalizeindex) print(churn_by_contract) # 画堆叠柱状图直接展示各组流失比例 churn_by_contract.plot(kindbar, stackedTrue, figsize(8, 5)) plt.title(Churn Rate by Contract Type) plt.ylabel(Proportion) plt.xticks(rotation0) plt.show() # 在网时长按流失分组看分布差异 sns.histplot(datadf, xtenure, hueChurn, multiplestack, bins40) plt.title(Tenure Distribution by Churn) plt.show()normalizeindex是这张图的关键参数按合同类型分组后把每组内部的 Yes/No 数量归一化成比例而不是显示绝对数量。原因很简单月付用户基数大直接堆绝对数会让人误以为“月付用户流失更多是因为月付本身有问题”归一化后才能看清组内差异避免基数误导。multiplestack把两个群体的直方柱上下堆叠而不是并排适合样本量差异大的场景——流失客户只有 1800 多人非流失 5000 多人并排画几乎看不出流失客户那根矮柱子堆叠后反而能在低 tenure 段看到明显的颜色对比。实际业务中电话销售最关心这批人刚入网几个月、按月付费、没有签长期合同。3.3 可视化参数同一张图不同审美不代表不同结论很多人会在配色和样式上反复纠结我的建议是这套模板直接抄。sns.set_style(whitegrid)在浅色背景上加网格线适合与运营同事对着屏幕讨论数据分类特征的countplot不要用默认调色板流失分析通常用蓝橙对比来区分两个群体。# 统一调色板蓝橙对比对色弱读者也友好 palette {No: #4C72B0, Yes: #C44E52} sns.countplot(datadf, xContract, hueChurn, palettepalette) plt.show()hueChurn搭配palette字典相当于把每个合同类型再按流失与否拆成两根柱子颜色语义固定蓝色代表留存、橙色代表流失。这里有一个细节是hue的顺序seaborn 默认按类别字母序排列No 在前 Yes 在后如果你希望 Yes 在左边可以显式传hue_order[Yes, No]。这类参数不影响模型效果但会影响汇报时的阅读效率值得在项目说明里记一笔。4. 建模预测用逻辑回归和随机森林跑出可解释的流失概率环境上装好 pandas、scikit-learn、matplotlib、seaborn 就可以开始建模版本不太挑Python 3.8 以上配 scikit-learn 1.x 都能跑通。建模阶段的核心不是堆模型而是先把特征工程做干净、把评估指标定对否则后面调参都是空中楼阁。4.1 特征编码一键独热和二值化的边界建模前必须把所有特征转成数值。这个数据集里的特征分三类真正的二值字段如 Partner、Dependents取值只有 Yes/No多分类字段如 Contract、PaymentMethod还有 SeniorCitizen 这种本来就是 0/1 的整数。二值字段用字典映射多分类字段用get_dummies做 one-hot。# 复制一份用于建模的数据避免污染原始表 X df.drop(columns[customerID, TotalCharges, Churn]).copy() y (df[Churn] Yes).astype(int) # 二值字段手动映射语义清晰且可控 binary_cols [Partner, Dependents, PhoneService, PaperlessBilling, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies] for col in binary_cols: X[col] X[col].map({Yes: 1, No: 0}) # gender 单独处理Male 映射为 1 X[gender] X[gender].map({Male: 1, Female: 0}) # 多分类字段 one-hot删掉第一列避免共线 X pd.get_dummies(X, columns[Contract, PaymentMethod, InternetService], drop_firstTrue) # 检查编码后的维度预期在 20 列左右 print(X.shape)这段代码里有几个参数值得说明。drop_firstTrue会为每个多分类字段少生成一列比如 Contract 本来有 Month-to-month、One year、Two year 三个取值编码后只保留两列基线类别由截距项吸收。这样既避免了虚拟变量陷阱又不会丢失信息。OnlineSecurity 这类字段的原始取值是 Yes/No/No internet service这时候map({Yes: 1, No: 0})会把 “No internet service” 映射成 NaN。我的做法是直接把它也映射为 0因为“没有这个服务”和“不购买这个服务”在流失预测的语义上等价都是“该客户在此项上没有付费附加服务”。这一步很多人踩坑后面特征重要性出现 NaN 报错才回头查。4.2 数据集划分stratify 是基本素养数据划分这件事看似简单实际决定模型评估的可信度。流失样本只占 26%如果随机划分训练集和测试集的流失比例会出现波动导致验证指标忽高忽低。stratifyy的作用是让训练集和测试集的标签分布保持一致。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 验证划分后的流失比例是否一致 print(y_train.mean(), y_test.mean())test_size0.2是这类小样本数据集的常用比例7043 行数据留出 1400 行测试既够评估又不会让训练集太薄。random_state42是复现实验的关键不设这个参数每次跑出来的指标都会不一样项目说明里也没法记录基线结果。4.3 逻辑回归基线class_weight 不能省逻辑回归在这类项目里最适合当基线模型训练快、系数可解释、部署简单。但直接用默认参数跑会因为类别不平衡而把所有样本预测为不流失。class_weightbalanced会自动给少数类更高的惩罚权重是这里最关键的参数。from sklearn.linear_model import LogisticRegression lr LogisticRegression(max_iter1000, class_weightbalanced, C0.5, random_state42) lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) y_prob_lr lr.predict_proba(X_test)[:, 1]max_iter1000是收敛保障one-hot 之后特征维度变多默认的 100 次迭代常常达不到收敛阈值就停了。C0.5是正则强度的倒数C 越小正则越强这个数据集特征之间相关性高稍微加点正则比默认的C1.0更稳AUC 通常能高 0.01 到 0.02。predict_proba(X_test)[:, 1]取的是“流失概率”而不是“不流失概率”后面阈值调优和名单导出都依赖这个概率值。4.4 随机森林与特征重要性黑匣子里的秩序逻辑回归适合做基线随机森林则用来捕捉非线性关系。随机森林的好处是不用太操心特征缩放、能输出特征重要性坏处是调参不当时很容易过拟合。下面这组参数是我在类似数据集上反复试过的一组稳定配置。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth6, min_samples_leaf5, n_jobs-1, random_state42, ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) y_prob_rf rf.predict_proba(X_test)[:, 1] # 查看特征重要性前 10 位 importance pd.Series(rf.feature_importances_, indexX_train.columns).sort_values(ascendingFalse) print(importance.head(10))n_estimators300不是越高越好300 棵树的精度增益已经很小但这个规模能让指标更稳定不会因为随机性波动太大。max_depth6是控制过拟合的核心参数树太深会在训练集上记住个别客户的行为min_samples_leaf5要求每个叶子节点至少 5 个样本进一步平滑预测。n_jobs-1表示用满所有 CPU 核心这个数据集虽然小但 300 棵树并行训练能明显节省时间。特征重要性输出通常很稳定tenure、MonthlyCharges、Contract 相关的列排在最前面。这个结果和可视化的结论互相印证说明特征工程没有做歪。4.5 评估指标怎么看别让 accuracy 骗了你流失预测的评估要同时看四个指标不能只盯着准确率。下面这张表给出了每个指标的适用场景指标含义在流失场景里的作用Accuracy全部样本里预测正确的比例参考即可类别不平衡下虚高Precision预测为流失的客户里真正流失的比例决定电话销售资源的浪费程度Recall真实流失客户里被找出来的比例决定能挽回多少客户ROC-AUC正负样本排序能力的综合指标模型整体排序能力最稳定的评估基准from sklearn.metrics import classification_report, roc_auc_score print(classification_report(y_test, y_pred_lr)) print(Logistic Regression AUC:, roc_auc_score(y_test, y_prob_lr)) print(Random Forest AUC:, roc_auc_score(y_test, y_prob_rf))逻辑回归和随机森林在这类数据上的 AUC 通常在 0.82 到 0.88 之间随机森林略高一点点但差距不大。如果逻辑回归的 AUC 超过 0.9不要高兴太早很可能是 TotalCharges 或 tenure 泄漏了标签信息具体原因写在下一章。5. 模型翻车排查样本不平衡、数据泄漏和随机性逐一拆解这部分是血泪经验。这类项目翻车的点非常集中下面五条是我在多个类似项目里反复踩过的坑每条按“现象→原因→解决”的格式记录可以直接对照排查。5.1 数据泄漏TotalCharges 一进特征AUC 直接 0.95现象把 TotalCharges 放进特征后逻辑回归的 AUC 一下飙到 0.95 以上看起来模型性能极好但实际上毫无可用价值。原因TotalCharges 是累计消费金额它本身就是在网时长和月消费的累加结果。一个客户已经在网 60 个月TotalCharges 自然很高而流失客户大多在网时间短TotalCharges 天然偏低。模型学到的不是“这类客户会流失”而是“账单累计金额低的客户会流失”这等于把结果的一部分当成了原因。解决建模特征里直接删除 TotalCharges只保留 tenure 和 MonthlyCharges。tenure 也会和流失相关但它的业务逻辑是“新客户容易流失”这是一个可利用的信号而不是泄漏。判断标准很简单这个特征是否在“预测时刻”就能拿到且不包含未来信息。TotalCharges 里混杂了客户从入网到当下的全过程信息不适合做预测特征。5.2 类别不平衡全预测不流失准确率也能到 74%现象用默认参数跑逻辑回归输出的准确率约 74%看起来不错查混淆矩阵发现测试集里所有样本都被预测为“不流失”流失客户一个都没找出来。原因数据里流失客户只占 26%模型只要全部预测为不流失准确率就有 74%这是类别不平衡下的典型假象。如果不改任何参数模型的损失函数会倾向于牺牲少数类来换取整体准确率。解决两步走。第一步在模型层面加class_weightbalanced让流失样本的误分类代价更高第二步在评估层面放弃单独看 accuracy改成重点看 recall 和 ROC-AUC。如果你还想要更细致的控制可以用class_weight{0: 1.0, 1: 2.5}这种手动权重但一般balanced就够用。5.3 随机性同一份代码两次训练结果不一样是正常的现象同一份数据、同一个模型两次运行得到的 AUC 差 0.02 左右新手容易以为是代码写错了或者环境有问题。原因随机森林内部每次抽样建树都依赖随机数train_test_split的划分也依赖随机数。不固定随机种子结果必然浮动这是黑匣子模型的正常行为。解决凡是涉及随机性的步骤都固定random_state。train_test_split设 42随机森林设 42逻辑回归设 42。固定之后结果应当完全可复现。如果固定种子后结果仍然不稳定检查是不是用了别的库的并行逻辑、或者数据读取顺序是否被外部改动过。5.4 编码顺序Yes/No 映射反了特征重要性全是乱的现象跑出来的特征重要性里gender 排到了前三业务上完全解释不通。原因map({Yes: 1, No: 0})如果写反成{Yes: 0, No: 1}模型学到的方向会完全反转。诊断方法很简单检查裸模型里该特征的系数符号或者直接打印X.head()看编码后的数值是否符合直觉。解决编码后手动抽查几列确认 Partner 为 Yes 的样本编码为 1。另外map遇到未在字典里的值会变成 NaN建模前用X.isna().sum().sum()检查整体缺失确保没有编码遗漏。这个检查十秒钟能跑完能省掉后面排查特征重要性异常的半天时间。5.5 特征冗余流媒体和互联网服务的共线问题现象特征重要性前十名里StreamingTV、StreamingMovies、OnlineSecurity 各自占了一席但删掉其中任何一列AUC 几乎不变。原因这些附加服务特征高度依赖 InternetService 字段。没有互联网服务的客户在这些列上的取值会大量重复one-hot 之后产生冗余维度。随机森林会把重要性分散到多个相关列上稀释真实信号。解决把多个二值服务列合并成一个“附加服务数量”数值特征或者只保留信息增益最大的两个列。常见做法是# 把 5 个附加服务特征合并为服务数量减少冗余 service_cols [OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies] X[service_count] X[service_cols].sum(axis1) X X.drop(columnsservice_cols)合并之后特征维度下降模型的可解释性提高AUC 基本不受影响。这个处理特别适合项目说明里写“特征工程”章节能体现你理解了特征之间的关系而不仅仅是在堆列。6. 把模型用起来概率阈值调优与 TopK 流失名单导出模型训练完只是第一步真正交给运营用的东西是一份“流失概率名单”。模型默认在 0.5 概率处切分但这个阈值在这个数据集上并不合理——流失率只有 26%用 0.5 当阈值会漏掉大量真实流失客户。更通用的做法是遍历阈值找业务上可接受的平衡点。import numpy as np from sklearn.metrics import recall_score, precision_score, f1_score thresholds np.arange(0.20, 0.75, 0.05) for t in thresholds: pred (y_prob_rf t).astype(int) print(ft{t:.2f} recall{recall_score(y_test, pred):.3f} fprecision{precision_score(y_test, pred):.3f} ff1{f1_score(y_test, pred):.3f})这里能看到很典型的趋势阈值越低recall 越高、precision 越低。比如阈值 0.30 时 recall 能到 0.8 以上但 precision 可能在 0.4 左右意味着电话销售打给 10 个人里只有 4 个会流失如果预算充足、挽回一个客户的收益远高于联系成本就选低阈值反之选高阈值。这个决策没有标准答案要跟业务方对齐。选定阈值后把概率最高的客户名单导出来这是项目的最终交付物# 把测试集索引对应的客户 ID 和流失概率拼到一起 result pd.DataFrame({ customerID: df.loc[X_test.index, customerID].values, churn_prob: y_prob_rf, tenure: df.loc[X_test.index, tenure].values, MonthlyCharges: df.loc[X_test.index, MonthlyCharges].values, }) # 按阈值筛选并排序输出给运营 target result[result[churn_prob] 0.35].sort_values(churn_prob, ascendingFalse) target.to_csv(output/churn_risk_list.csv, indexFalse) print(f共筛出 {len(target)} 个高流失风险客户)df.loc[X_test.index, customerID]是关键因为建模前删过缺失值X_test的索引和原始表能对上用loc回联客户 ID 而不是直接拼接避免顺序错位。导出 CSV 用indexFalse保证运营打开文件时没有多余的索引列。最后落到项目说明上这份源码的价值在于把“数据清洗→可视化→建模→名单导出”这条链路串了起来你拿到后真正要投入精力的是两件事——把阈值调优脚本跑一遍理解不同阈值下名单规模的变化以及把可视化章节里的四张图替换成你自己业务数据的版本。我自己的习惯是每个项目都保留一个threshold_report.txt把不同阈值下的指标和筛出人数记下来方便两个月后回来调整阈值时对照。这样维护的不只是模型而是一套可以持续迭代的决策工具。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。