基于Python的电信客户流失预测:从数据清洗到多模型对比实战

📅 发布时间:2026/10/9 18:22:33
基于Python的电信客户流失预测:从数据清洗到多模型对比实战
简介这份资源是一份面向初学者与高校学生的客户流失预测实验报告以电信用户数据为案例完整演示如何用Python搭建流失预测模型适合作为期末报告参考或机器学习入门练手项目。压缩包内共1个doc文件约1.42MB内容为山东财经大学Python大数据分析课程的实验报告涵盖实验目的、环境配置、数据字段解释、实验步骤与模型选择等模块。报告基于7044条电信用户数据、20个字段使用Windows 10与Jupyter Notebook结合NumPy、Pandas、Matplotlib、Seaborn、Scikit-learn等库依次完成数据导入与探索、编码转换与标准化、特征工程并对比随机森林、支持向量机、逻辑回归、KNN与朴素贝叶斯等算法的准确率、召回率和F1值最终选出较优模型。目前已有1465人学习下载读者可借此理解客户流失分析的业务背景、掌握从数据清洗到模型评估的完整流程并参考报告结构撰写自己的实验文档。1. 从一份电信数据集说起客户流失预测到底能落地成什么手里这份资源是一套完整的客户流失预测实验流程基于 Python 3.7 和 Jupyter Notebook用一份 7043 行、21 列的电信用户数据集从数据清洗一路走到多模型对比。它解决的不是预测未来这种虚问题而是一个很具体的业务场景手里有一批用户的行为和套餐数据想知道哪些人下个月大概率要走好提前做留存动作。适合谁看如果你正在做数据分析课程设计、期末实验报告或者刚转行想找一个端到端的分类项目练手这份流程的完整度是够的——它覆盖了缺失值处理、类别编码、标准化、相关性分析、多算法对比这一整条链路。但要注意它是一份教学性质的实验记录不是生产级方案直接搬到线上会踩坑。下面我按数据怎么进、特征怎么造、模型怎么选、坑在哪的顺序拆一遍。2. 数据清洗与类型转换把 21 列脏数据变成能喂模型的表2.1 先看清数据长什么样拿到任何一份 CSV第一步永远是确认形状和字段类型而不是急着建模。这份数据集读取后是 (7043, 21)但注意正文里shape显示 7043 行而描述里写的是 7044 条这种差一行的情况在真实数据里很常见通常是有表头被算进去了或者末尾有空行以shape的实际输出为准。import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from pylab import rcParams from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.model_selection import StratifiedShuffleSplit from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix import warnings warnings.filterwarnings(ignore) # 读取数据路径按自己实际存放位置改 telcom pd.read_csv(rD:/kehuliushi/WA_Fn-UseC_-Telco-Customer-Churn.csv) print(telcom.shape) # (7043, 21) print(telcom.info()) # 看每列 dtype 和非空数量 print(telcom[Churn].value_counts())info()这一步是关键它能一次性暴露两个问题哪些列是 object 类型需要编码哪些列存在空值。这份数据里TotalCharges是 object 类型说明里面混了非数字字符这是后面必须处理的第一个雷。2.2 TotalCharges 的类型陷阱TotalCharges表面看是费用应该是浮点数但info()显示它是 object。原因通常是新入网用户tenure0总费用字段是空字符串而不是 0。直接to_numeric会报错必须加errorscoerce把无法转换的变成 NaN。# 强制转数值无法转换的置为 NaN telcom[TotalCharges] pd.to_numeric(telcom[TotalCharges], errorscoerce) print(telcom[TotalCharges].dtype) # float64 print(pd.isnull(telcom[TotalCharges]).sum()) # 11 # 缺失值只有 11 条占比极低直接删行 telcom.dropna(inplaceTrue) print(telcom.shape) # (7032, 21)这里有个参数选择值得说11 条缺失值占 7032 的 0.16%删掉完全合理。但如果缺失比例超过 5%就不能无脑删得考虑用中位数填充或者单独标记。我一般会先算一下缺失比例再决定errorscoerce这个参数是必加的不加直接抛异常。2.3 目标变量编码与不均衡认知Churn是 Yes/No模型不认字符串要转成 1/0。同时要意识到这份数据是不均衡的No 有 5174 条Yes 只有 1869 条流失率约 26.6%。# 目标变量二值化 telcom[Churn].replace(to_replaceYes, value1, inplaceTrue) telcom[Churn].replace(to_replaceNo, value0, inplaceTrue) print(telcom[Churn].value_counts()) # 0 5174 # 1 186926.6% 的流失率在分类问题里算中等不均衡不算极端。这意味着后面评估模型时不能只看准确率——一个全预测不流失的模型准确率就有 73.4%但毫无业务价值。所以召回率和 F1 才是重点这一点在选模型那章会展开。3. 特征工程相关性分析怎么砍掉无用列、one-hot 怎么选3.1 用 factorize 快速看相关性在正式编码前先用pd.factorize把类别列临时转成整数算一个粗略的相关性矩阵目的是快速定位哪些特征和流失强相关、哪些特征之间高度共线。charges telcom.iloc[:, 1:20] # factorize 把每个类别映射成整数仅用于看相关性 corrDf charges.apply(lambda x: pd.factorize(x)[0]) corr corrDf.corr() plt.figure(figsize(20, 16)) sns.heatmap(corr, xticklabelscorr.columns, yticklabelscorr.columns, linewidths0.2, cmapYlGnBu, annotTrue) plt.title(Correlation between variables) plt.show()factorize和LabelEncoder的区别要清楚factorize返回编码后的数组和唯一值索引适合快速探索LabelEncoder是 sklearn 的转换器适合放进 pipeline。这里只是看相关性用factorize更省事。从热力图能读出两组强相关互联网服务、网络安全、在线备份、设备保护、技术支持、网络电视、网络电影这一组互相高度正相关多线业务和电话服务强相关。强共线特征对逻辑回归这类线性模型不友好但对树模型影响不大所以是否删列要看后面用什么模型。3.2 one-hot 编码与 Churn 相关性排序探索阶段用 factorize正式建模要用 one-hot因为类别之间没有大小关系强行映射成 1/2/3 会让模型误以为光纤 DSL 无。# 对除 customerID 外的所有列做 one-hot tel_dummies pd.get_dummies(telcom.iloc[:, 1:21]) print(tel_dummies.shape) # 看各变量与 Churn 的相关性排序 plt.figure(figsize(15, 8)) tel_dummies.corr()[Churn].sort_values(ascendingFalse).plot(kindbar) plt.title(Correlations between Churn and variables) plt.show()排序结果里gender和PhoneService的值接近 0说明这两个变量对流失几乎没影响可以直接删。这是特征选择最朴素也最有效的一招相关性绝对值低于某个阈值我一般用 0.05的类别特征先删掉再看效果。3.3 合并冗余类别与标签编码数据里有一类特殊值No internet service和No phone service。从业务上看没开通互联网服务的用户在网络安全、在线备份这些字段上都是No internet service本质上和No是一回事合并能减少类别数、降低维度。telcomvar telcom.iloc[:, 2:20] telcomvar.drop(PhoneService, axis1, inplaceTrue) telcom_id telcom[customerID] # 合并冗余类别 telcomvar.replace(to_replaceNo internet service, valueNo, inplaceTrue) telcomvar.replace(to_replaceNo phone service, valueNo, inplaceTrue) # 对剩余 object 列做标签编码 telcomobject telcomvar.select_dtypes([object]) for col in telcomobject.columns: telcomvar[col] LabelEncoder().fit_transform(telcomvar[col])这里有个容易翻车的点LabelEncoder对Contract这种有序类别按月 一年 两年是合理的但对PaymentMethod这种无序类别编码成 0/1/2/3 会引入虚假的大小关系。教学流程里为了简化统一用了 LabelEncoder生产环境更稳妥的做法是对无序类别用 one-hot对有序类别用有序编码。这是这份资源需要读者自己补上的认知。3.4 数值特征标准化tenure、MonthlyCharges、TotalCharges三个数值列量纲差异大TotalCharges动辄几千tenure最大 72不标准化会让距离类算法KNN、SVM被大数值主导。scaler StandardScaler(copyFalse) # 先 fit_transform 看结果再 transform 写回 telcomvar[[tenure, MonthlyCharges, TotalCharges]] \ scaler.fit_transform(telcomvar[[tenure, MonthlyCharges, TotalCharges]]) # 箱线图检查异常值 plt.figure(figsize(8, 4)) sns.boxplot(datatelcomvar[[tenure, MonthlyCharges, TotalCharges]], paletteSet2) plt.title(Check outliers) plt.show()标准化后三个变量都在均值 0、方差 1 附近箱线图显示没有明显离群点。注意fit_transform只能用在训练集上测试集必须用训练集的均值和方差做transform否则就是数据泄露。这份教学流程在划分数据集之前就做了标准化严格来说是有泄露风险的后面避坑章会专门讲。4. 模型训练与评估分层抽样、多算法对比与指标选择4.1 为什么必须用 StratifiedShuffleSplit数据不均衡时普通随机划分可能让某一折里正样本极少甚至没有导致评估结果波动大。分层抽样保证每一折里正负样本比例和整体一致。X telcomvar y telcom[Churn].values sss StratifiedShuffleSplit(n_splits5, test_size0.2, random_state0) print(分组数, sss.get_n_splits(X, y)) for train_index, test_index in sss.split(X, y): X_train, X_test X.iloc[train_index], X.iloc[test_index] y_train, y_test y[train_index], y[test_index] break # 取第一折演示参数说明n_splits5表示切 5 组test_size0.2表示每组里测试集占 20%random_state0固定随机种子保证可复现。break是为了只取第一折做演示实际评估应该循环 5 折取平均。4.2 多模型横向对比这份资源的价值之一是把常见分类器都跑了一遍。下面用统一流程对比逻辑回归、随机森林、SVM、KNN、朴素贝叶斯。from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import precision_score, recall_score, f1_score models { LogisticRegression: LogisticRegression(max_iter1000), RandomForest: RandomForestClassifier(n_estimators100, random_state0), SVM: SVC(), KNN: KNeighborsClassifier(), NaiveBayes: GaussianNB(), DecisionTree: DecisionTreeClassifier(random_state0) } for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_test) print(f--- {name} ---) print(Precision:, round(precision_score(y_test, y_pred), 4)) print(Recall: , round(recall_score(y_test, y_pred), 4)) print(F1: , round(f1_score(y_test, y_pred), 4))参数上LogisticRegression的max_iter1000是必须调的默认 100 在标准化后的数据上经常不收敛RandomForest的n_estimators100是起点树太少方差大太多训练慢100 到 300 之间比较常见。4.3 指标怎么读才不误导跑完对比你会发现准确率普遍在 75% 到 80% 之间但召回率差异很大。流失预测的业务本质是宁可错杀不可放过——漏掉一个真要走的客户损失比误判一个不走的客户大得多。所以选模型时优先看召回率和 F1而不是准确率。模型准确率参考召回率参考适用场景逻辑回归约 0.80约 0.55需要可解释系数随机森林约 0.79约 0.50特征重要性分析SVM约 0.79约 0.52小样本高维KNN约 0.76约 0.48快速基线朴素贝叶斯约 0.75约 0.65召回优先朴素贝叶斯召回率往往偏高但精确率低适合先圈一批人再人工筛的场景。随机森林和逻辑回归更均衡。具体数值以你实际跑出来的为准这里给的是量级参考。4.4 混淆矩阵看漏判from sklearn.metrics import confusion_matrix import seaborn as sns best_model RandomForestClassifier(n_estimators100, random_state0) best_model.fit(X_train, y_train) y_pred best_model.predict(X_test) cm confusion_matrix(y_test, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(Actual) plt.title(Confusion Matrix) plt.show()混淆矩阵右下角是真正例正确预测流失左下角是假负例实际流失但没预测出来。假负例的数量直接对应业务损失这个数字比准确率更值得盯。5. 避坑与排查这份教学流程里最容易翻车的五个点5.1 现象模型准确率虚高到 90% 以上原因标准化或编码在划分训练测试集之前就做了测试集信息泄露到训练过程。这份流程里StandardScaler和LabelEncoder都在划分之前执行严格来说有泄露。解决把预处理放进Pipeline或者手动先划分再对训练集fit、对测试集transform。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000)) ]) pipe.fit(X_train, y_train) print(pipe.score(X_test, y_test))5.2 现象TotalCharges 转换后多出一批 NaN原因新用户tenure0总费用是空字符串to_numeric默认报错加errorscoerce后变成 NaN。解决先统计 NaN 数量占比低就删占比高就用 0 或中位数填充并单独加一个是否新用户标记列。5.3 现象逻辑回归报 lbfgs failed to converge原因默认max_iter100不够或者特征没标准化导致梯度下降慢。解决max_iter调到 1000 以上同时确认数值特征已标准化。如果还不行换solversaga试试。5.4 现象LabelEncoder 编码后模型效果反而变差原因对无序类别如 PaymentMethod用了标签编码引入了虚假顺序关系。解决无序类别改用pd.get_dummies或OneHotEncoder有序类别如 Contract保留标签编码。5.5 现象每次跑结果都不一样原因没固定random_state或者用了StratifiedShuffleSplit但没在模型里固定种子。解决数据划分、模型初始化、交叉验证三处都设random_state保证实验可复现。6. 进阶技巧用特征重要性和阈值调优把召回率再拉一截跑通基础流程后真正拉开差距的是两件事知道哪些特征在起作用以及会调分类阈值。先看随机森林的特征重要性这比相关性矩阵更可靠因为它考虑了特征之间的交互rf RandomForestClassifier(n_estimators200, random_state0) rf.fit(X_train, y_train) import pandas as pd feat_imp pd.Series(rf.feature_importances_, indexX_train.columns) feat_imp.sort_values(ascendingFalse).head(10).plot(kindbarh) plt.title(Top 10 Feature Importance) plt.show()通常排前面的是Contract、tenure、TotalCharges、MonthlyCharges、InternetService。这几个特征和业务直觉一致合同越短、在网时间越短、费用越高的用户越容易走。第二件事是调阈值。默认 0.5 的分类阈值在流失预测里往往偏保守把阈值降到 0.3 到 0.4能显著提升召回率y_prob rf.predict_proba(X_test)[:, 1] for thresh in [0.3, 0.4, 0.5, 0.6]: y_pred_thresh (y_prob thresh).astype(int) print(f阈值 {thresh}: 召回{recall_score(y_test, y_pred_thresh):.4f}, f精确{precision_score(y_test, y_pred_thresh):.4f})阈值降到 0.3 时召回率通常能涨 10 到 15 个百分点代价是精确率下降误判增多。具体取多少取决于留存动作的成本——如果发一张优惠券成本很低就大胆降阈值如果人工回访成本高就保守一点。我自己的习惯是每次做完一个分类项目都会把阈值调优和特征重要性这两步固定加进流程而不是跑完默认模型就交差。因为默认参数和默认阈值只是起点真正决定业务效果的是这两步的微调。这份资源把完整链路铺好了剩下的就是你在它基础上补上工程化的部分。希望帮到你。本文还有配套的精品资源点击获取