
简介面向机器学习初学者与算法竞赛实践者Python基于机器学习的电信用户流失预测项目以Kaggle平台上的Telco Customer Churn数据集为对象完整演示从业务理解到模型验证的端到端流程。项目按三个典型阶段展开业务背景解读与数据探索包括数据分布检验、正确性校验、质量评估及训练集与测试集一致性检查数据重新编码与模型训练覆盖常规建模步骤特征衍生与筛选涉及批量特征衍生、Filter筛选方法、海量特征衍生与筛选并附特征筛选总结图。压缩包共128个文件核心为7个Jupyter Notebook分步笔记、1个Python脚本及1份csv原始数据另含98个png图表与10张jpg/jpeg图片直观呈现特征分布与筛选结果10个md说明文档则用于记录方法心得与步骤要点整体仅10.21MB配套文件按阶段组织目录清晰便于对照复现。目前已有431人学习下载适合希望在真实业务数据上快速掌握机器学习项目全流程、积累特征工程与建模经验的读者。1. 电信用户流失预测先想清楚这个模型到底在预测什么如果只挑一个反直觉的结论我会说在电信用户流失预测里准确率是最不值得看的指标。一个把所有人都预测为“不流失”的模型在流失率只有 26% 的数据上准确率能到 74%但这个模型对业务毫无价值。这个项目标题要做的事情就是用 Python 把机器学习的分类流程完整走一遍怎么定义流失、怎么从原始用户表里造特征、怎么处理类别不平衡、怎么选模型和定评估口径最终产出一份运营真的会拿去打电话的流失名单。适合有 Python 基础、但还没系统跑过分类项目的人也适合想直接抄一套特征工程加训练评估代码骨架的忙人。这里踩过的坑不少我会把代码和翻车现场一起讲清楚。2. 数据准备与特征工程把电信用户表变成可训练的特征集2.1 流失标签和原始字段先看数据再动手常见的电信流失数据集是用户维度的宽表每一行代表一个用户字段覆盖三类信息用户属性性别、是否老年用户、是否有伴侣和家属、服务开通情况电话、网络、安全服务、技术支持等、账单与合同月账单、总账单、合同类型、付款方式。标签列一般叫 ChurnYes 代表该用户已流失。我拿到数据后不会直接开跑先做三件事看行数列数、看流失比例、看字段类型。import pandas as pd df pd.read_csv(telco_churn.csv) print(df.shape) print(df[Churn].value_counts(normalizeTrue)) print(df.dtypes)这段代码的输出会告诉你三个关键信息样本量是否足够、正负样本是否失衡、哪些字段是字符串。电信流失数据里流失用户占比通常在 20% 到 30%这个比例意味着“猜所有人不流失”的准确率基线就是 70% 以上模型必须明显超过这个值才有意义。字段类型直接影响后续编码方式像 Contract、PaymentMethod 这类对象列要做编码而 tenure、MonthlyCharges 是数值列可以直接用。2.2 字段清洗TotalCharges 和 tenure 的两个陷阱流失预测项目里第一个坑往往藏在 TotalCharges 这个字段里。它看起来是数值实际上在 CSV 里经常被存储成字符串直接做数值运算会报错或者被 pandas 转成 NaN。更隐蔽的是新入网用户还没有产生账单TotalCharges 缺失是正常业务现象不能粗暴填 0。填 0 等于告诉模型“这个用户没花过钱”模型会把它学成强流失信号但实际原因是用户刚来。# TotalCharges 是字符串转换时非数字会被变成 NaN df[TotalCharges] pd.to_numeric(df[TotalCharges], errorscoerce) # 新入网用户还没有出账单缺失是正常的 # 用 tenure * MonthlyCharges 估算比直接填 0 更符合业务逻辑 mask df[TotalCharges].isna() df.loc[mask, TotalCharges] df.loc[mask, tenure] * df.loc[mask, MonthlyCharges] # tenure 与流失的关系通常是非线性的前半年流失率高之后快速下降 df[tenure_group] pd.cut( df[tenure], bins[0, 6, 12, 24, 48, 100], labels[0-6m, 6-12m, 1-2y, 2-4y, 4y] )第一行转数值时用 errorscoerce把无法解析的非数字变成 NaN而不是直接报错。接下来用 tenure 乘以 MonthlyCharges 估算缺失的总费用逻辑是“一个用了 5 个月、月费 60 的用户总费用应该在 300 左右”这个估算不引入额外偏差。最后的 tenure 分段是我在这个项目里的习惯操作原始 tenure 是连续数值但流失率随在网时长的变化近似指数衰减直接喂原始值给线性模型模型学到的是一条直线拟合远不如分箱后表达得清楚。2.3 编码与数据划分别让泄漏提前发生字符串字段不能直接进模型常见做法是用 LabelEncoder 或者 OneHotEncoder。在电信流失数据上我一般对二分类字段比如 Partner、PhoneService用 LabelEncoder对多分类字段比如 Contract、PaymentMethod用 OneHotEncoder 或者直接交给树模型做原生类别编码。这里有个容易忽略的点customerID 是唯一标识绝对不能进入特征矩阵否则模型会直接记住每个用户的 ID训练集上表现完美验证集上立刻崩掉。from sklearn.preprocessing import LabelEncoder X df.drop(columns[customerID, Churn]) y df[Churn].map({Yes: 1, No: 0}) object_cols X.select_dtypes(includeobject).columns for col in object_cols: X[col] LabelEncoder().fit_transform(X[col]) 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(X_train.shape, X_test.shape) print(y_train.mean(), y_test.mean())train_test_split 里的 stratifyy 是流失预测项目里必须写的参数。它保证训练集和测试集里的流失比例与全量数据一致避免随机划分时某一折里全是流失用户或者全是不流失用户。random_state 固定成 42 是为了结果可复现。划分完之后检查 y_train.mean() 和 y_test.mean() 是否接近如果两者差很多说明划分出了问题后面所有评估都会失真。3. 逻辑回归基线把流失预测流程完整跑通3.1 为什么拿逻辑回归当基线流失预测本质是二分类问题第一个模型我不建议直接上 XGBoost。逻辑回归在工程上有三个不可替代的优势训练速度快几秒出结果系数可以直接解释成“这个特征对流失概率的正向或负向影响”和业务方对齐时能说清楚“为什么给这个用户打高分”。先把基线跑通确认数据、特征、评估链路是好的再换复杂模型才有意义。逻辑回归对数值尺度敏感所以要先做标准化。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression lr_pipeline Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(max_iter1000, random_state42)) ]) lr_pipeline.fit(X_train, y_train)Pipeline 的作用是把标准化和模型训练绑成一个整体避免你在训练集上算好了均值方差、却忘了在测试集上做同样处理。StandardScaler 会把 tenure、MonthlyCharges 等数值列变成均值为 0、方差为 1 的分布这样逻辑回归的梯度下降收敛更快系数大小也能横向比较。max_iter 设成 1000 是因为默认的 100 在特征维度稍高时容易出现不收敛警告调大以后一劳永逸。3.2 类别不平衡SMOTE 的用法和边界流失数据天然不平衡逻辑回归在这种数据上会把所有样本的概率都往低处压因为多数类不流失在损失函数里占了绝对主导。常见的处理手段是 SMOTE用插值的方式合成少数类样本。注意一个红线SMOTE 只能作用在训练集上测试集必须保持真实分布否则验证结果毫无意义。from imblearn.over_sampling import SMOTE smote SMOTE(random_state42) X_train_res, y_train_res smote.fit_resample(X_train, y_train) print(SMOTE 前正样本数:, sum(y_train 1)) print(SMOTE 后正样本数:, sum(y_train_res 1))SMOTE 的原理是在两个相邻的少数类样本之间做线性插值生成新的合成样本。它解决的是“模型没见过足够多正样本”的问题但不会创造新的信息。做完之后正样本数量会和不流失样本持平。我不建议把 SMOTE 当成必选项如果树的模型对不平衡更鲁棒可以跳过这一步直接调 scale_pos_weight但对逻辑回归这种线性模型SMOTE 带来的提升通常很明显。3.3 评估指标准确率会骗人AUC 和召回率才是主心骨模型训练完第一件事不是看准确率而是看 AUC 和召回率。AUC 衡量的是模型把流失用户排在不流失用户前面的能力不依赖阈值适合做模型选型。但业务上真正关心的是召回率流失用户里有多大比例被捞出来了。如果模型预测的流失概率 Top 20% 能覆盖真实流失用户的 60% 以上这个名单就已经有营销价值了。from sklearn.metrics import roc_auc_score, recall_score, classification_report y_proba lr_pipeline.predict_proba(X_test)[:, 1] y_pred (y_proba 0.5).astype(int) print(AUC:, roc_auc_score(y_test, y_proba)) print(默认阈值 0.5 的召回率:, recall_score(y_test, y_pred)) print(classification_report(y_test, y_pred))predict_proba 取第二列拿到的是正类流失的概率。0.5 只是一个默认阈值在这个项目里它并不一定合适。当你看到 classification_report 里召回率只有 40% 左右时不要慌这是逻辑回归在类别不平衡数据上的正常表现。后面两个方向可以改进一是加 SMOTE 重训二是在树模型上调 scale_pos_weight。这两个方向我会在下一章展开。4. 树模型调优用 LightGBM 把 AUC 再往上拉4.1 从逻辑回归换到树模型的原因逻辑回归能做的树模型基本都能做而且通常做得更好。原因有三个树模型能自动捕捉特征之间的非线性交互比如“合同是月付 在网小于 6 个月 没有技术支持”这种组合信号对特征尺度不敏感不用做标准化对缺失值有原生处理策略。在电信流失数据集上LightGBM 的 AUC 通常比逻辑回归高 3 到 5 个百分点训练时间却只有几十秒。我这边的经验是先跑一个不调参的 LightGBM 作为上界参考再回头决定要不要在特征上继续投入。import lightgbm as lgb # scale_pos_weight 按负样本 / 正样本的比例给正样本加权 neg sum(y_train 0) pos sum(y_train 1) model_lgb lgb.LGBMClassifier( n_estimators300, learning_rate0.05, max_depth6, num_leaves31, scale_pos_weightneg / pos, random_state42 ) model_lgb.fit(X_train, y_train)4.2 n_estimators、learning_rate、max_depth 三个参数怎么定LightGBM 调参的优先级也是有讲究的。第一个要定的是 learning_rate 和 n_estimators 的组合learning_rate 越小模型越稳但需要的树越多训练越慢。0.05 配 300 棵树是一个起步组合跑完看训练集和测试集的 AUC 差距如果训练集 AUC 接近 1 而测试集明显低就是过拟合调低 n_estimators 或加大 max_depth 的惩罚。max_depth 控制单棵树的深度默认无限制但对表格数据我习惯限制在 4 到 8 之间防止树记住个别异常用户。from sklearn.metrics import roc_auc_score y_proba_lgb model_lgb.predict_proba(X_test)[:, 1] print(LightGBM AUC:, roc_auc_score(y_test, y_proba_lgb)) # 对比逻辑回归基线的 AUC print(Logistic Regression AUC:, roc_auc_score(y_test, y_proba))scale_pos_weight 这个参数在这个项目里比 SMOTE 更好用。它的原理是给正样本的损失函数乘一个权重数值上等于负样本数除以正样本数。用之前算好的 neg / pos不用手动改。它的好处是不改变测试集分布不像 SMOTE 有泄漏风险。我在这类项目里的习惯是LightGBM 优先调 scale_pos_weight逻辑回归优先用 SMOTE。4.3 特征重要性看模型到底在学什么模型效果稳定之后一定要看特征重要性。这一步不是为了发论文而是为了验证模型学到的规律符合业务直觉。如果特征重要性排第一的是 customerID说明特征泄漏了前面做的清洗有漏洞如果排前面的全是合同类型、在网时长、月费说明模型学到的东西和业务常识一致这份名单拿去给运营解释时才有人信。importance pd.Series(model_lgb.feature_importances_, indexX_train.columns) top_cols importance.sort_values(ascendingFalse).head(12) print(top_cols)这段代码输出的是每个特征在树分裂中被使用的次数。LightGBM 的 feature_importances_ 默认基于 split 次数它衡量的是“这个特征被用来分裂的频繁程度”不是严格意义上的因果重要性但用来排查泄漏和确认特征有效性已经够用。在电信流失数据上Contract、tenure、TotalCharges、MonthlyCharges 排进前五基本是正常现象。如果你的结果里 OnlineSecurity 和 TechSupport 排很前也不用惊讶这符合“服务越全、用户越不容易走”的业务经验。5. 避坑流失预测项目最常见的五个翻车现场5.1 翻车一对全量数据做 SMOTE验证集也跟着变样现象训练完模型后测试集 AUC 到 0.95满怀信心上线实际名单命中率惨不忍睹。原因SMOTE 被错误地应用在了全量数据上包括测试集。测试集里的合成样本和训练集样本高度相似模型相当于在“开卷考试”里被评分。解决SMOTE 必须放在 train_test_split 之后只对 X_train 和 y_train 做。写完代码后自查一遍执行顺序建议把 SMOTE 放进 Pipeline 或者用函数封装避免线下验证时手滑。5.2 翻车二TotalCharges 转数值出现 NaN填 0 后模型把“新用户”错判成“高危用户”现象模型输出名单里大量新入网用户月费很低、在网很短、总费用为 0。原因TotalCharges 在字符串转数值时产生 NaN直接 fillna(0) 后模型把“总费用为 0”当成强流失信号而新用户天然满足这个条件。解决用 tenure 乘以 MonthlyCharges 估算缺失值或者保留 NaN 交给 LightGBM 的 missing 处理。千万不要为了省事填一个和业务含义冲突的常数。5.3 翻车三customerID 泄漏进特征特征重要性第一居然是 ID现象训练集 AUC 0.99特征重要性排名第一是 customerID测试集 AUC 掉到 0.75。原因customerID 是每个用户的唯一标识模型直接把它当成记忆工具等于背答案。解决读入数据后第一步就把 customerID 从特征矩阵里删掉。我习惯在 pd.read_csv 之后立即执行 df df.drop(columns[customerID])而不是等到特征工程最后再做因为中间任何一步 merge 或者操作都可能把 ID 带回来。5.4 翻车四随机划分数据模型在“未来”上失效现象验证集 AUC 很高上线第一个月名单命中率还行第二个月开始明显下滑。原因流失预测本质是时间序列问题用随机划分训练集和测试集等于让模型用 8 月的数据预测 9 月同时训练集里也混着 9 月的数据时间信息被提前泄漏了。解决按时间切分前 80% 月份的数据训练后 20% 月份的数据验证。如果数据集没有时间列至少用 GroupShuffleSplit 按用户 ID 分组保证同一个用户不会被同时分到训练集和验证集。5.5 翻车五阈值固定死在 0.5名单要么太少要么太多现象模型 AUC 0.85但把阈值定在 0.5 后预测为正的样本只有几百个运营嫌名单太少调到 0.3 后名单几千个命中率又下降。原因0.5 是默认值不是最优值。在不平衡数据里逻辑回归输出的概率普遍偏低模型可能把所有用户的流失概率都压在 0.3 以下。解决用验证集上的召回率-精确率曲线或者 PR 曲线来找阈值。先确定业务目标运营一天能跟进的客户数是固定的比如 500 人那就取预测概率 Top 500 作为名单而不是卡死在某个固定阈值。6. 模型持久化与完整预测把流失名单真正交到业务手里6.1 用 joblib 保存训练产物项目做完不是把模型留在 Jupyter Notebook 里就结束。实际工程里要把模型文件、训练时用到的特征列名、编码器全部保存下来形成一个可复用的预测包。joblib 是 pickle 的替代品对 numpy 数组和大对象效率更高sklearn 官方推荐用它保存模型。import joblib import pandas as pd # 保存模型和训练时用到的列名 joblib.dump(model_lgb, churn_model.pkl) joblib.dump(X_train.columns.tolist(), feature_columns.pkl) # 新用户表进来加载模型和列名 loaded_model joblib.load(churn_model.pkl) feature_cols joblib.load(feature_columns.pkl) new_data pd.read_csv(new_customers.csv) new_X new_data[feature_cols].copy() new_proba loaded_model.predict_proba(new_X)[:, 1]这里有一个非常容易踩的坑预测时必须保证新数据的列和训练时完全一致顺序也要一致。DataFrame 的列顺序变了模型拿到的特征矩阵在内存里就是另一回事。所以加载 feature_columns.pkl 并用它做列过滤和重排是预测脚本里必须写的一步。6.2 阈值校准根据运营产能反推分数卡点模型输出的是概率业务要的是名单。我的做法是先在验证集上计算不同阈值下的召回率和精确率再和运营确认“一天最多联系多少人”直接取概率 Top N。假设验证集有 1000 个用户真实流失 260 人取 Top 200 时命中了 130 人那么名单命中率就是 65%这是可以直接拿出来和业务对齐的指标。threshold 0.35 candidates new_data.loc[new_proba threshold, [customerID, telephone]] candidates[churn_score] new_proba[new_proba threshold]阈值 0.35 只是一个示例实际项目中应该基于验证集的表现动态决定。我一般会把验证集按概率排序输出“Top 100 命中率、Top 200 命中率、Top 500 命中率”三档给运营选让他们根据人力来定名单大小而不是替他们拍板。6.3 上线后验证闭环模型上线不是终点。我这边吃过最大的亏是上线后三个月才去复盘命中率业务早就悄悄不用名单了。后来我养成一个习惯每月模型输出名单后记录这批用户当月的实际流失率和历史随机人群的流失率对比算出一个提升倍数。这个倍数低于 1.5 倍时就该检查数据分布是否变了、特征是否失效、是否需要重新训练。这种闭环验证不复杂一张 Excel 表就能维护但它是模型能否长期被业务信任的关键。阈值怎么定、名单怎么交、上线后怎么验证这套东西和训练模型同样重要。单看模型指标逻辑回归和 LightGBM 都够看真正让项目发挥价值的是把概率转成运营愿意跟进的名单并且持续跟踪命中率。希望帮到你。本文还有配套的精品资源点击获取