简介:这份资源是面向高校学生与数据挖掘初学者的电信客户流失预测与分析完整项目源码,可作为数据挖掘课程期末大作业、课程设计或自学练手使用。项目基于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 直接读 CSV:
import 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"], errors="coerce") # 查看转换后缺失情况 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配合errors="coerce"把无法转换的值也变成 NaN。填充策略选择 tenure 乘月费,是因为总费用本质上就是历史月费累加,这个近似在业务上站得住。参数说明:errors="coerce"表示无法转换时赋 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, columns=multi_cat_cols, drop_first=True) # 删除无用的客户 ID 字段 df = df.drop(columns=["customerID"]) # 确认最终特征矩阵 print(df.shape) print(df.columns.tolist())逻辑说明:drop_first=True是为了避免虚拟变量陷阱,即多个 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_size=0.2, random_state=42, stratify=y ) print("训练集流失比例:", y_train.mean()) print("测试集流失比例:", y_test.mean())逻辑说明:stratify=y保证划分后正负样本比例与原始数据一致,避免测试集里流失样本过少导致评估不稳定。参数说明:test_size=0.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_iter=1000, class_weight="balanced", random_state=42) 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(ascending=False).index).head(10))逻辑说明:class_weight="balanced"让模型自动调整权重,缓解类别不平衡。max_iter=1000防止迭代次数不够导致不收敛。AUC 比准确率更适合不平衡数据,因为它衡量的是排序能力而不是阈值切分。参数说明:predict_proba返回每个样本属于各类的概率,取第二列是流失概率。系数为正表示该特征增加流失风险,为负表示降低风险。
4.3 随机森林与特征重要性
逻辑回归之后,用随机森林看非线性关系,同时输出特征重要性:
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=200, max_depth=10, min_samples_leaf=5, class_weight="balanced", random_state=42, 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", ascending=False) print(importance.head(10))逻辑说明:n_estimators=200是树的数量,越多越稳定但计算越慢。max_depth=10限制树深防止过拟合。min_samples_leaf=5保证叶子节点至少有 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_type="bar") # 单个样本的解释 shap.force_plot( explainer.expected_value[1], shap_values[1][0], X_test.iloc[0], matplotlib=True )逻辑说明:TreeExplainer是专门给树模型用的,计算速度快。shap_values[1]取的是流失类别的 SHAP 值。summary_plot的 bar 类型展示全局重要性,beeswarm 类型展示每个特征的影响方向。force_plot展示单个样本的预测分解,红色推高流失概率,蓝色拉低。参数说明:explainer.expected_value是模型在训练集上的平均预测值,作为基准线。
实际用的时候,我一般会挑几个典型样本做 force_plot:一个高概率流失的、一个低概率流失的、一个模型判错的。把这三个图的解释写进报告,比堆一堆模型指标有说服力得多。另外 SHAP 值可以按特征聚合后和业务字段做交叉,比如看看合同类型为月付的客户里,哪些特征在推高流失风险,这样分析就落到了业务动作上。
有个习惯我保持了几年:每次做完一个分类项目,都会把 SHAP 图和混淆矩阵放在同一页对比看。混淆矩阵告诉你错在哪,SHAP 告诉你为什么错。两个结合起来,模型就不再是黑匣子,而是一个能对话的分析对象。希望帮到你。
本文还有配套的精品资源,点击获取