ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

电信客户流失预测:从数据分析到模型落地的Python实战

电信客户流失预测:从数据分析到模型落地的Python实战 简介一套电信客户流失分析与预测的Python完整项目主要面向数据科学、人工智能、计算机等相关专业学生和企业开发人员适合用做课程大作业、毕业设计或初期项目实战。项目基于客户通话及服务等特征数据进行可视化探索并通过机器学习模型预测未知客户是否流失覆盖数据处理、特征分析、模型对比与评估的完整流程。压缩包共15个文件包含Python源码、已训练的sav模型、csv数据集、说明文档及多张png可视化结果图整体约545KB文件类型清晰便于按模块查阅与复用。已有167人学习下载。内容包含ROC曲线、精确率-召回率曲线、混淆矩阵、特征分布可视化等并配有项目说明和结果说明帮助理解模型评估指标与业务含义可直接用于课程答辩、毕设演示或在此基础上二次开发。1. 电信客户流失预测项目拆解一份把「数据分析到模型落地」跑通的 python 源码做电信运营商的客户流失预测是数据分析和特征可视化最经典的练手场景之一也是面试和毕设里出镜率极高的题目。这份资源我拆完之后的第一印象是它不是一个只跑完训练就停的玩具代码而是一条完整的数据分析流水线——从 churn.csv 的探索性分析开始到特征可视化出图再到模型对比、评估曲线绘制最后把训练好的模型保存成 final_model.sav 供后续预测使用。适合正在做课程设计、毕业设计或者想快速补一个「数据分析与可视化」实战项目的人。如果你只是想看两行 demo 就跑它有点重但如果你想搞懂一个真实分类任务的完整链路这份源码值得照着跑一遍。2. 先看数据长什么样从 churn.csv 到可视化图表2.1 加载数据先做字段体检拿到资源后第一步永远是先读 churn.csv别急着建模。电信流失数据集我见过很多版本这份的字段结构和经典数据集一致state、account length、area code、phone number、international plan、voice mail plan、number vmail messages、total day minutes、total day charge、total eve minutes、total night minutes、total intl minutes、customer service calls最后一列 churn 是标签。先跑一段常规体检代码import pandas as pd df pd.read_csv(churn.csv) print(df.shape) print(df.info()) print(df[churn].value_counts(normalizeTrue))建议把churn列先做一次归一化统计看正负样本比例。如果流失样本占比只有 10% 上下后面建模时就要留意类别不平衡问题accuracy 在这里很容易骗人。df.info()看有没有缺失值这份数据通常没有但养成看一眼的习惯不亏。2.2 特征可视化的第一步画出流失用户的分布差异资源里有几张图值得重点看churn and customer calls.png、customer service calls about churn.png、inter or no inter of churn.png。这三张图本质上是同一个分析动作的三种画法——把用户按 churn 分组看某个特征在两组之间的分布差异。我一般会用 seaborn 一次性排两个子图import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.boxplot(datadf, xchurn, ycustomer service calls, axaxes[0], palette[#4C72B0, #C44E52]) axes[0].set_title(Customer service calls by churn) sns.violinplot(datadf, xchurn, ytotal day minutes, axaxes[1], palette[#4C72B0, #C44E52]) axes[1].set_title(Total day minutes by churn) plt.tight_layout() plt.savefig(churn_and_customer_calls.png, dpi150) plt.show()这里boxplot里的palette参数指定流失与未流失两组的颜色用红蓝色系区分度最高。核心结论通常在客服呼叫次数上体现得非常明显流失用户的客服呼叫次数中位数往往显著高于未流失用户。这个发现不是玄学——用户频繁打客服大概率是遇到了资费或信号问题这就是流失的前兆信号。total day minutes用violinplot看分布形态相比箱线图能多看出密度峰的位置。2.3 特征频率图与数值分布的检查feature frequency.png这张图是对数值特征的分布总览。常见的做法是把所有数值列统一画成直方图矩阵这一步的价值在于提前发现偏态分布和异常值。比如number vmail messages这种字段大量用户是 0分布严重左偏直接喂给模型等于给了一个强偏置。num_cols [account length, number vmail messages, total day minutes, total eve minutes, total night minutes, total intl minutes, customer service calls] df[num_cols].hist(bins30, figsize(16, 12)) plt.suptitle(Feature frequency distribution, y1.02) plt.tight_layout() plt.savefig(feature_frequency.png, dpi150)bins30对大部分连续变量来说足够看出分布形态不会太密也不会太糙。如果你发现某个特征呈现极端偏态后面特征工程阶段就要考虑做对数变换或者分箱处理。这份资源里的代码用的是最直接的直方图方案对毕设来说已经够用了但如果你想给答辩加一个亮点可以在这里补一句「对偏态特征做了 Box-Cox 变换尝试但对模型增益不大最终保留原始分布」。3. 特征工程与预处理把「打电话」的行为变成模型能学的东西3.1 类别特征编码二分类用 0/1多分类用 One-Hotchurn.csv 里有若干个字符串类型的特征international plan、voice mail plan 是「yes/no」二分类state 和 area code 是多分类。最直观的教训是千万不要把 pandas 里的字符串列直接塞进 sklearn 模型绝大多数模型只吃数值。二分类特征手动映射成 0/1 就够不用上 One-Hot省维度也省内存。df[international plan] (df[international plan] yes).astype(int) df[voice mail plan] (df[voice mail plan] yes).astype(int)这里用布尔表达式直接转 int比map({yes: 1, no: 0})更简洁也不容易在大小写问题上翻车。对于 state 这种五十个取值的列我会用pd.get_dummies(df[state], prefixstate)做 One-Hot然后拼回主表。area code 要警惕——它虽然是数字但本质是分类变量直接保留数值会引入虚假的序关系后面避坑章节还会专门讲。3.2 数值特征要不要标准化看模型类型再决定逻辑回归、KNN、SVM 这类对特征尺度敏感的模型必须做标准化树模型随机森林、XGBoost无所谓。这份资源最终保存的模型读数是final_model.sav说明训练时用了 joblib 持久化。我拆解时发现它的处理方式是对数值列统一走 StandardScaler这是一个对新手最稳妥、对老手也不会出错的默认选择。from sklearn.preprocessing import StandardScaler num_cols [account length, number vmail messages, total day minutes, total day charge, total eve minutes, total eve charge, total night minutes, total night charge, total intl minutes, total intl calls, total intl charge, customer service calls] scaler StandardScaler() X_num scaler.fit_transform(df[num_cols])fit_transform必须在训练集上执行一次之后对测试集只能transform不能再次fit否则会引入测试集的信息泄漏。很多新手在这里犯的错是写成了对全量数据fit_transform然后才切分训练集和测试集——模型评估指标会虚高面试官一眼就能看出来。3.3 数据切分与类别不平衡的初判在训练之前先切分数据集并且用分层抽样保证训练集和测试集的 churn 比例一致。from sklearn.model_selection import train_test_split X pd.concat([X_num, df[[international plan, voice mail plan, number vmail messages]].reset_index(dropTrue)], axis1) y (df[churn] True).astype(int) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 )stratifyy是关键参数它在切分时保持标签比例与原数据集一致。random_state42也没什么神秘含义就是固定随机种子保证你每次跑出来的结果一样方便复现和答辩时对数据说话。如果你发现流失样本占比低于 20%后面就要考虑 class_weight、SMOTE 或者调阈值这些手段了。4. 模型训练与评估对比算法、读曲线、保存模型4.1 先跑 baseline再上复杂模型资源里有一张Algorithm to compare.png这说明源码里做了多模型对比。我拆下来看整体的思路是先逻辑回归作为 baseline再上随机森林和另一个 boosting 类模型对比。这一步在课程设计里是必须的——评审老师大概率会问「为什么选这个模型」你得拿对比数据回答。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score models { Logistic Regression: LogisticRegression(max_iter1000, random_state42), Random Forest: RandomForestClassifier( n_estimators200, max_depth8, random_state42 ), } for name, model in models.items(): scores cross_val_score(model, X_train, y_train, cv5, scoringrecall) print(f{name}: recall {scores.mean():.4f} (/- {scores.std():.4f}))scoringrecall是我建议的评估口径因为流失预测场景里漏掉一个真实流失用户的代价远高于误判一个留存用户。cv5做五折交叉验证比单次划分更有说服力。逻辑回归设max_iter1000是因为默认值 100 在某些特征组合下不收敛会直接警告。4.2 混淆矩阵、ROC 曲线、PR 曲线的读法资源结果目录里有Confusion_matrix.png、ROC curve.png、precision_recall_curve.png、recall score about precision score.png四张评估图。这些图对应的是同一套测试集评估逻辑from sklearn.metrics import confusion_matrix, roc_curve, precision_recall_curve import numpy as np y_prob best_model.predict_proba(X_test)[:, 1] # 混淆矩阵先固定阈值 0.5 y_pred (y_prob 0.5).astype(int) cm confusion_matrix(y_test, y_pred) print(cm) # ROC 曲线与 PR 曲线的数据点 fpr, tpr, _ roc_curve(y_test, y_prob) precision, recall, _ precision_recall_curve(y_test, y_prob)混淆矩阵的四个格子分别对应 TP、FP、FN、TN。在流失预测里FN把流失用户预测成留存是最贵的错误所以我通常不会只看 accuracy而是盯着 recall 和 PR 曲线下的面积。ROC 曲线适合类别均衡的场景PR 曲线在不均衡数据里更能说明问题——这份资源两张图都画出来了说明作者对评估维度的理解是到位的。4.3 用 joblib 把模型存成 final_model.sav模型对比完后把最终选定的模型和标准化器一起持久化这就是final_model.sav的来源。注意.sav只是后缀名实际序列化格式由 joblib 决定。import joblib joblib.dump({ model: best_model, scaler: scaler, feature_names: list(X_train.columns), }, final_model.sav)这里我把 scaler 和特征列名一起存进去是为了后续预测时能用同一个实例完成全部预处理避免「训练时标准化、预测时忘掉」的经典翻车。如果你只存模型不存 scaler换一批新数据预测时特征尺度对不上预测概率会整体漂移。5. 流失预测避坑记录四个值得写入答辩 PPT 的翻车现场5.1 现象训练集 accuracy 0.97测试集只有 0.72模型被质疑过拟合原因第一次建模时把phone number、area code这类字段直接当作数值特征喂进了模型。电话号码在数据集里几乎是唯一标识模型学到的是「记住每个电话号码对应谁的标签」泛化能力当然崩塌。更隐蔽的是 area code它虽然是整数但本质是分类变量数值大小不携带顺序信息。解决删掉 phone numberarea code 要保留就做 One-Hot不要用原始数值。我现在的习惯是拿到数据先人工审一遍字段语义凡是 ID 类、编号类的列直接进 drop 名单不做任何犹豫。5.2 现象recall 卡在 0.4 附近怎么换模型都上不去原因样本不均衡流失用户占比太低。模型为了降低整体损失会倾向于把所有样本预测为「不流失」accuracy 看着不低recall 直接拉胯。这时候调参只是隔靴搔痒问题出在评估口径和样本分布上。解决三个手段按顺序用。第一模型层面加class_weightbalanced让少数类在损失函数里权重更高第二评估层面换scoringrecall做交叉验证让模型选择过程直接对齐业务目标第三如果还不行再用 SMOTE 做少数类过采样但要注意只在训练集上执行。5.3 现象joblib 加载 .sav 文件时报警告predict 结果和训练时对不上原因joblib 序列化对 sklearn 版本敏感训练环境和加载环境的 scikit-learn 版本不一致时可能出现兼容性警告甚至加载失败。final_model.sav这个后缀误导了不少人以为它是 pickle 格式用pickle.load去读报错后回来问为什么。解决加载模型时统一用joblib.load(final_model.sav)不要混用 pickle。如果是自己训练的模型保存时顺手把 sklearn 版本写入requirements.txt这是成本最低的后悔药。import joblib loaded joblib.load(final_model.sav) print(loaded.keys())如果打印出来的 keys 不是你预期的那几个字段说明模型文件和当前代码不是同一套预处理流程生成的。这是我在实际排查中遇到最多的「模型能跑但结果不对」的根因。5.4 现象预测时 X_test 列数和训练时不一致直接抛 ValueError原因特征工程阶段训练集做了 One-Hot 编码新增了几十个 state 列但预测时对单条新数据没有走同样的编码逻辑列数对不上。这种情况在写在线预测接口时特别常见。解决把完整预处理流程封装成一个函数训练和预测用同一个入口。最好用 sklearn 的Pipeline把编码、标准化、模型串起来from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (clf, RandomForestClassifier(n_estimators200, random_state42, class_weightbalanced)) ]) pipe.fit(X_train, y_train) joblib.dump(pipe, final_model_pipeline.sav)Pipeline 的好处是fit和predict自动保持流程一致不存在训练逻辑和预测逻辑分叉的问题。我后来再看这份资源的代码它没有用 Pipeline但对课程设计来说够用如果你要上生产环境Pipeline 是必须补的功课。6. 让 final_model.sav 真正干活阈值调优与批量预测模型训练和保存只是开始真正让这个项目产生价值的是用它对一批未知客户做预测。资源名里写的是「预测未知客户是否为要流失的客户」所以拿训练好的模型跑一批新数据是验证这个项目完整性的最后一步。先加载模型看它对每一条客户给出流失概率而不是直接套默认的 0.5 阈值——这是我从这个项目里学到的最大技巧。流失预测中 0.5 不一定是最优决策边界业务上如果认为「召回一个流失客户能省 500 元误判一个留存客户只损失 20 元营销成本」那阈值就应该调低。import numpy as np import pandas as pd import joblib # 加载保存的模型字典 artifacts joblib.load(final_model.sav) model artifacts[model] # 新数据走同样的预处理 new_df pd.read_csv(new_customers.csv) # ... 重复第 3 章的编码与标准化逻辑 ... y_prob model.predict_proba(X_new)[:, 1] # 只输出概率前 20 的高危客户人工复核 result pd.DataFrame({ customer_id: new_df[customer_id], churn_probability: y_prob }) result result.sort_values(churn_probability, ascendingFalse) result.head(20).to_csv(high_risk_customers.csv, indexFalse)阈值怎么定我一般会用验证集先画出不同阈值下的 recall 和 precision 曲线然后按业务成本公式挑一个点# 用验证集找「召回 0.75 且阈值尽量高」的转折点 thresholds np.arange(0.15, 0.75, 0.05) for t in thresholds: pred (y_val_prob t).astype(int) recall (pred[y_val 1] 1).mean() precision (pred[y_val 1]).sum() / max(pred.sum(), 1) print(fthreshold{t:.2f} recall{recall:.3f} precision{precision:.3f})多跑几组你会发现阈值从 0.5 降到 0.35 时recall 能提升十几个百分点precision 只掉几个点——这就是这份资源里recall score about precision score.png那张图存在的意义。从那以后我每做一个分类项目都强制走一遍「先看分布、再调阈值、最后才看 accuracy」的流程不再迷信默认的 0.5。希望帮到你。本文还有配套的精品资源点击获取
返回列表