简介这是一份面向大数据技术类专业师生的《Python机器学习编程与实战》第8章教学教案针对通信运营商客户流失分析与预测场景讲解从数据预处理到分类建模的完整流程。教案覆盖数据去重、PCA降维、缺失值与异常值处理、独热编码、训练集与测试集划分以及MLP多层感知器模型的构建与评估并配有教学目标、重点难点和理论/实验教学过程设计。压缩包内为1个PDF文件整体约64KB已有1791人学习浏览。内容还设计了引导性、探究性和拓展性问题可帮助教师组织课堂互动也能指导学生围绕客户挽留策略、不同分类算法对比等方向做深入实践适合作为备课讲义、自学提纲或实验参考。1. 客户流失预测不是玄学一套从清洗到 MLP 的完整流程我拆过两份通信运营商客户流失的教案这份第 8 章属于那种“看着平平无奇、跑一遍才发现门道不少”的资源。数据里几百个客户、几十个特征流失用户占比不到 15%如果你上来就直接丢给模型训练准确率能到 90% 以上但那基本等于废话。这套教案真正的价值是把机器学习里最容易翻车的部分——数据预处理和特征工程——按教学顺序拆成了去重、降维、缺失值与异常值处理、独热编码、MLP 建模、模型评价六个完整环节配套有数据、代码和引导性问题。适合带课老师、刚入门想做完整案例的新手以及想复用这套流程做用户运营分析的人。下面按我完整跑通一遍的次序来说。2. 数据清洗四件套去重、降维、缺失值异常值一个都别省2.1 去重先查完全重复行再决定 ID 列要不要丢拿到数据第一步永远是去看有没有重复记录。通信运营商的原始数据经常来自多个系统导出的拼接同一个用户被记了两遍非常常见。pandas 里用df.duplicated()可以统计完全重复的行具体做法是import pandas as pd df pd.read_csv(telecom_customer.csv) print(原始行数:, len(df)) print(完全重复行数:, df.duplicated().sum()) df df.drop_duplicates() print(去重后行数:, len(df))这里df.duplicated()默认按所有列一起判断是否重复返回一个布尔序列sum()统计 True 的个数。drop_duplicates()默认保留第一次出现的行把后面重复的删掉。如果你的数据里存在“只有某几列相同但其他列不同”的情况想按指定列去重可以加subset参数比如df.drop_duplicates(subset[customer_id])意思是同一个 customer_id 只保留一行。这一步看起来简单但有个容易忽略的坑customer_id 这类标识列不能直接当特征丢给模型。我在实际跑这份教案时见过有人把 ID 列留在特征里结果 MLP 学到了“ID 是某几个值的人流失”这种完全无意义的规律。教案里把这步放在第(1)位是有道理的去重不只是为了减少重复样本更是为了让模型去学习真实的用户行为规律而不是学习数据录入时的重复痕迹。2.2 降维PCA 是手段不是目的先问列是不是真冗余降维这一步容易被两种极端处理毁掉一种是根本不降维几十个特征直接硬塞给 MLP另一种是上来就 PCA完全不看原始列的业务含义。教案把降维放在去重之后目的是让模型训练更快、也减少过拟合。常见做法是先看数值型特征之间的相关性高度相关的列考虑删掉然后再决定要不要 PCA。我的习惯是先跑一段相关性检查corr df_numeric.corr() high_corr [(i, j) for i in corr.columns for j in corr.columns if i ! j and abs(corr.loc[i, j]) 0.8] print(高相关特征对数:, len(high_corr))如果对数很少说明列本身不算冗余可以跳过 PCA如果一堆列互相之间相关系数超过 0.8比如通话时长和通话费用这种必然强相关的组合就需要处理。教案里用的是 PCA实战中我做 PCA 之前一定会先做标准化否则通话分钟数这种量纲大的列会把整个主成分方向带偏from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler StandardScaler() scaled scaler.fit_transform(df_numeric) pca PCA(n_components12) df_pca pca.fit_transform(scaled) print(累计方差贡献率:, pca.explained_variance_ratio_.cumsum())解释一下参数n_components12表示保留 12 个主成分具体保留多少个没有定数一般做法是看累计方差贡献率加到 85% 以上就够。explained_variance_ratio_输出每个主成分解释的方差比例cumsum是累计求和。如果第一个主成分就解释了 60% 以上的方差说明原始列之间确实存在高度冗余PCA 是合理的如果前几个主成分都很平均说明特征各自独立这时候硬降维反而是损失信息。这里要特别提醒一句PCA 出来的主成分是人看不懂的综合变量教案里做降维更多是为了教学演示实际业务中如果目标是“根据模型结果反推运营策略”我一般会优先考虑直接删冗余列而不是 PCA。这个取舍在教案的探究性问题里也有涉及降维的影响是“减少过拟合、加速训练”和“损失可解释性”之间的权衡。2.3 缺失值与异常值先量化缺失比例再决定填充策略缺失值处理是整个数据清洗里最容易“想当然”的一步。教案里要求掌握缺失值和异常值的检测与处理但我见过不少学生的做法是一上来就df.fillna(df.mean())完全不管每列缺失了多少、为什么缺失。我的做法是先量化missing_ratio df.isnull().mean() print(missing_ratio[missing_ratio 0].sort_values(ascendingFalse))df.isnull().mean()计算的是每列缺失值占比。参考经验是缺失比例超过 30% 的列直接删掉因为一条列缺失三分之一以上填充出来的值基本是编的缺失比例在 5% 以内数值列用中位数填充、分类列用众数填充中间地带通常看列的业务重要性决定。填充的代码不复杂df[total_charge].fillna(df[total_charge].median(), inplaceTrue) df[international_plan].fillna(df[international_plan].mode()[0], inplaceTrue)为什么数值列常用中位数而不是均值因为均值对异常值敏感如果这个客户的通话费用分布右偏、少数高消费用户拉高了平均值用均值填充会高估普通用户的费用水平。中位数更稳健。异常值检测常用的 IQR 方法代码是Q1 df[total_day_minutes].quantile(0.25) Q3 df[total_day_minutes].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR outliers df[(df[total_day_minutes] lower) | (df[total_day_minutes] upper)] print(IQR法识别异常值数量:, len(outliers))IQR四分位距是第三四分位数减第一四分位数1.5 倍 IQR 是经典判定区间落在区间外的视为异常。注意这里有个实战上的坑不要一识别出异常值就删。在通信运营商场景里通话分钟数极高的人可能是商务用户或高价值用户直接删掉反而把最值得关注的客户样本删没了。正确做法是先看异常值占比如果只有 1%-2%单独分析他们是否构成一个合理群体如果异常值很多说明数据分布本身就偏考虑换用分位数截断而不是 1.5 倍 IQR。3. 独热编码与数据合并把类别列变成模型能读的数字3.1 数字编码的陷阱1、2、3 不是大小关系通信运营商数据里有很多分类列国际套餐international_plan是“是/否”语音邮箱voice_mail_plan也是“是/否”还有地区州名。初次接触机器学习的同学最容易犯的错误是直接用 pandas 的factorize或者 sklearn 的LabelEncoder把类别变成 0、1、2、3 的整数。这个做法在“是/否”这种二分类列上问题不大但一旦类别超过两个比如州名有几十个LabelEncoder 就会给类别赋予大小顺序。模型看到数字 3 2 1会默认类别之间存在顺序关系学到的是根本不存在的规律。这是独热编码存在的根本原因把每个类别拆成一列独立的 0/1 特征互不干扰。教案里的套餐类别就适合做独热编码。下面代码把两列 0/1 特征做独热编码看起来有点小题大做但它是理解整个编码思路的起点df_encoded pd.get_dummies( df[[international_plan, voice_mail_plan]], drop_firstTrue ) print(df_encoded.head())pd.get_dummies是 pandas 自带的一键独热编码columns参数指定需要转换的列。drop_firstTrue的意思是A、B 两个类别会被编码成两列 0/1但删掉第一列只保留一列用 0 和 1 就能区分两个类别这样能避免多重共线性。对特征少的逻辑回归模型尤其重要对 MLP 影响也有限但养成 drop 掉第一列的习惯没有坏处。3.2 用 OneHotEncoder 处理多分类列sparse_output 和 handle_unknown如果分类列超过两个取值比如 state 有 50 个州用get_dummies没问题但工程上更规范的做法是用 sklearn 的OneHotEncoder它可以和 Pipeline 配合、也能在测试集出现新类别时用handle_unknownignore避免报错。代码写法是from sklearn.preprocessing import OneHotEncoder encoder OneHotEncoder(sparse_outputFalse, handle_unknownignore) state_encoded encoder.fit_transform(df[[state]]) print(state_encoded.shape)这里sparse_outputFalse是为了返回普通 numpy 数组而不是稀疏矩阵后续方便和其他 DataFrame 拼接。如果你的 sklearn 版本较老这个参数叫做sparseTrue注意版本差异。handle_unknownignore的意思是训练时 state 只有 50 个类别测试数据里如果冒出一个没见过的州直接把这列全置 0而不是报错中断。需要注意OneHotEncoder 转换出的列名默认是 0、1、2 这样的序号丢了原来的类别名。想保留列名可以用get_feature_names_out()方法拿到新列名再套回 DataFrame。这个细节在教案的实验环节没细讲但实际做特征工程时几乎一定会遇到。3.3 合并数据concat 按行对齐merge 按键连接别搞混数据预处理到这里手里大概有三个部分清洗过的数值特征、PCA 降维后的分量或者直接是标准化数值列、独热编码后的类别特征。下一步是把它们拼回一张表。这一步看着简单却是我见过翻车率最高的环节之一。X_final pd.concat([df_numeric, df_encoded], axis1) print(X_final.shape)pd.concat的axis1是按列方向拼接条件是好几张表的行顺序完全一致。这里只要保证都来自同一个原始 DataFrame 的索引通常没问题。但如果你是从不同的 CSV 文件里分别读出特征和标签再用 concat 拼接行顺序万一不一样模型就在错误的对齐关系上训练了。那种场景应该用pd.merge按共同键连接X pd.merge(df_features, df_labels, oncustomer_id, howinner)merge的on参数指定连接键how参数指定连接方式。inner表示只保留两边都有的 customer_id。什么时候该用 concat、什么时候该用 merge判断标准就一条行之间有没有业务上的对应关系。有共同 ID 就用 merge只是同一个数据集拆出来再拼回去用 concat。合并之后有一个必须做的检查打印 shape 确认行数没变、列数符合预期。concat 默认是 outer join万一两张表索引没对齐行数会暴涨很多人直到训练时报维度错误才发现数据已经烂了。这个习惯直接来自我自己的翻车经历。4. 数据集划分与 MLP 训练参数怎么设才能让模型真的收敛4.1 train_test_split 的 stratify 参数类别不均衡时一定要用流失预测场景里流失用户占比通常只有 10% 左右。直接随机划分训练集和测试集有可能出现训练集里流失用户只剩 5%、测试集里占了 15% 的情况模型和评价结果都失真。train_test_split的stratify参数就是干这个的from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_final, y, test_size0.3, random_state42, stratifyy ) print(训练集流失占比:, y_train.mean()) print(测试集流失占比:, y_test.mean())参数含义test_size0.3表示 30% 数据做测试集stratifyy表示按 y 中类别的比例分层抽样保证训练集和测试集里的流失客户占比都和全量一致。random_state42固定随机种子让每次运行结果可复现——这个参数在调参时尤其重要不然你换一次参数就换一批数据分布没法比较。输出y_train.mean()看一眼两个集合的流失占比如果 stratify 没设这里的比例很可能差出几个百分点。4.2 特征缩放只对训练集 fit测试集只 transform数值特征如果包含通话时长、费用、客户年龄列量纲差距很大。MLP 这类基于梯度下降的模型对量纲敏感不缩放的话大数值列会主导损失函数。标准化是常见做法但有一个教学设计里经常不讲的细节scaler 不能在整个数据集上 fit。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)逻辑说明fit_transform在训练集上计算均值和标准差并完成缩放transform直接沿用训练集的均值和标准差去缩放测试集。如果对测试集也做fit_transform相当于测试集的统计信息提前泄漏进了模型测试结果就“虚高”了。这是数据泄漏的一种常见隐蔽形式。这一点我在用这份教案上课时反复强调学生最容易犯的错误就是把 scaler 对整个 X 做fit_transform再切训练集和测试集。从结果看模型指标很好但部署上线后完全不是那么回事。4.3 MLPClassifier 参数详解hidden_layer_sizes、activation、max_iter 怎么配MLP 是这份教案的核心算法。sklearn 里的 MLPClassifier 是前馈神经网络的最简实现参数不多但每个都影响训练结果。一份能直接跑的代码如下from sklearn.neural_network import MLPClassifier mlp MLPClassifier( hidden_layer_sizes(32, 16), activationrelu, solveradam, max_iter500, random_state42 ) mlp.fit(X_train_scaled, y_train)逐个说参数hidden_layer_sizes(32, 16)表示两层隐藏层第一层 32 个神经元、第二层 16 个。层数和神经元的数量没有标准答案教案里教的是一般从数据规模出发特征 30-50 个时两层每层 16 到 64 是常见起步区间过少学不到规律过多容易过拟合。activationrelu是隐藏层的激活函数ReLU 计算快、能缓解梯度消失是当前 MLP 的默认选择相比 tanh 收敛更快。solveradam是优化器Adam 自适应学习率绝大多数场景比 sgd 省心。max_iter500是最大迭代轮数几千样本的训练集通常 500 轮足够收敛但如果跑了 500 轮还没收敛sklearn 会弹出 ConvergenceWarning这时不是报错而是明示你迭代不够。训练完之后想确认模型是否真的收敛看 loss 曲线import matplotlib.pyplot as plt plt.plot(mlp.loss_curve_) plt.xlabel(iteration) plt.ylabel(loss) plt.show()mlp.loss_curve_是训练过程中每个迭代周期的损失值序列。曲线如果在末端已经变得平缓说明收敛了如果还在明显下降就停了说明 max_iter 不够调大到 1000 再跑。这个可视化是排查训练是否健康最直接的证据。这里我顺便提一下 activation 和 solver 的搭配。对这份数据量级的分类任务relu adam 是稳定组合tanh sgd 不是不行但收敛慢、对学习率敏感新手几乎必翻车。教案把 MLP 放在重点位置本质上就是让学习者一遍遍体会参数间的耦合关系。5. 模型评价与避坑指南准确率高不代表模型能用5.1 评价指标怎么读混淆矩阵、精确率、召回率、F1流失预测是最典型的类别不均衡分类问题——90% 客户不流失10% 流失。这时候准确率没有参考价值模型什么都不学全部预测“不流失”准确率也有 90%。教案里教学目标是掌握分类模型评价方法实际看的就是混淆矩阵和它派生出的三个指标。from sklearn.metrics import classification_report, ConfusionMatrixDisplay y_pred mlp.predict(X_test_scaled) print(classification_report(y_test, y_pred)) ConfusionMatrixDisplay.from_predictions(y_test, y_pred)classification_report输出精确率、召回率、F1 三列。在流失场景里召回率意味着“真正流失的客户里模型找回了多少”精确率意味着“预测会流失的人里有多少真的流失了”F1 是两者的调和平均类别不均衡时比准确率可靠得多。ConfusionMatrixDisplay直接画出 2x2 混淆矩阵左上角是预测正确的不流失客户右下角是预测正确的流失客户左下角是“预测不流失但实际流失”的漏报右上角是“预测流失但实际没流失”的误报。判断模型好坏不能只看一个数。想降低运营成本就压低误报想最大限度挽留客户就压低漏报成本和收益的权重决定了你更该优化哪个指标。5.2 避坑记录一标签泄漏让准确率虚高到离谱现象模型在测试集上误差极低准确率接近 95%但换一批真实数据预测完全不准。原因特征里混进了“只有在流失发生后才能知道”的变量比如用户已经产生的退费金额、投诉次数、最后的月消费额。这类列在训练集里和标签天然强相关模型学到的其实是“事后信息”而不是“事前征兆”。解决模型训练前逐列做信息保留检查把任何在预测时点根本拿不到的列全部删掉。这是做这门课实验时最隐蔽也最致命的一个坑。5.3 避坑记录二类别不均衡导致模型全预测成多数类现象classification_report里流失那一行的召回率接近 0测试集 90% 以上样本都被预测为不流失。原因MLP 默认的阈值 0.5 是在假设两类样本数量均衡的前提下设计的流失样本只有 10% 时模型输出的概率很难超过 0.5。解决给 MLPClassifier 加class_weightbalanced让少数类样本在损失函数里获得更高权重如果还不够就调低预测阈值比如predict_proba输出的概率超过 0.3 就判为流失用验证集选最优阈值。5.4 避坑记录三ConvergenceWarning 被当成报错现象训练完弹出一行 “ConvergenceWarning: Maximum iterations (500) reached without convergence”很多人以为模型坏了。原因不是报错是 sklearn 在提示你 max_iter 不够训练在达到迭代上限时被强制停止模型还没收敛。解决看mlp.loss_curve_曲线形状——末端还在下降就把 max_iter 加到 1000 或者 2000如果曲线已经平了还警告说明损失函数可能已经进入平坦区可以忽略警告。这一步属于最常见的“被警告吓住”问题。5.5 避坑记录四独热编码后维度爆炸训练直接变龟速现象做完整套预处理X_train_scaled的列数从 30 涨到 200MLP 训练一轮要等很久。原因state 这种高基数类别列没处理就get_dummies几十个州变成几十列加上其他类别列特征维度爆炸。解决先对低频类别做合并出现次数低于阈值比如总数 1%的类别统一替换成 “others”再做独热编码。这个操作能用极小的信息损失换掉大量冗余维度。我在跑这份教案时习惯在 get_dummies 之前加一段value_counts(normalizeTrue)检查每个类别的占比而不是直接编码。6. 进阶用法换分类器对比验证再用特征重要性反推业务6.1 统一数据口径下的算法对比MLP 不是唯一能解决流失预测的算法。教案的拓展性问题正好问到了“除了 MLP换其他分类算法是否效果更好”这个问题在实验里值得认真做一次对比验证。代码写法很简单from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier models { MLP: MLPClassifier(hidden_layer_sizes(32, 16), max_iter500), LogisticRegression: LogisticRegression(max_iter500), DecisionTree: DecisionTreeClassifier(max_depth5) } for name, model in models.items(): model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) print(name, classification_report(y_test, y_pred, zero_division0))注意这里统一用同一套缩放后的训练数据保证了对比的公平性。你会发现逻辑回归在类别不均衡时常常比 MLP 更容易调教而决策树的优势是能直接看feature_importances_反推业务含义。对运营团队来说特征重要性往往比模型准确率更有价值因为它告诉你该优先给哪些用户发挽留策略。如果想把 MLP 参数摸得更透网格搜索是省事的选择from sklearn.model_selection import GridSearchCV param_grid { hidden_layer_sizes: [(64,), (64, 32), (32, 16)], alpha: [0.0001, 0.001] } grid GridSearchCV( MLPClassifier(max_iter500), param_grid, cv3, scoringf1 ) grid.fit(X_train_scaled, y_train) print(grid.best_params_) print(grid.best_score_)GridSearchCV会在参数组合里逐个训练并交叉验证cv3表示 3 折交叉验证足够评估且不会等太久。scoringf1表示用 F1 作为择优指标——再次强调流失预测这类不均衡场景如果写scoringaccuracy搜索出的参数会偏袒多数类。这份教案跑完我自己的感受是从清洗到建模的每一步都可以通过指标对比来验证必要性某一列不删、某个填充方式换一下F1 的波动都比想象中大。从那以后我每次做完一个模型都强制自己再跑一遍特征删减实验用对比结果来说服自己而不是靠感觉。希望这次梳理能帮你在跑这个案例时少走几条弯路。本文还有配套的精品资源点击获取