资讯动态

基于平均值填充和逻辑回归的用户流失预测完整实战

发布时间:2026/10/17 3:49:09 来源:尧图企业网站定制
上个月我在处理一个用户流失预测的任务数据集拿到手第一眼就让人头大3万条记录十几个特征其中四个数值特征的缺失率都在15%上下还有个关键字段甚至丢了近三成。大多数人的第一反应是直接把空行删掉我早期也这么干过直到发现原本只有两个类别的标签在删行之后比例更失衡了模型训练出来的指标全是虚的。这次我换了一条完整路线先做平均值填充预处理再用逻辑回归Logistic Regression算法跑分类训练与评估。整条链路走下来踩了不少坑也把很多只知道调用却不知道原理的环节彻底理清了。这篇文章不是教科书是我把这套流程完整复盘了一遍从缺失值处理的选型逻辑到逻辑回归背后的数学直觉再到训练评估中容易被忽略的细节最后是实操时真正遇到的坑和应对思路。无论你是刚接触机器学习的新手还是已经跑过几个模型但想把细节抠明白的开发者这篇都能给你一些可参考的干货。1. 拿到带缺失值的数据集为什么我选了平均值填充先说结论平均值填充不是一个最优方案但在大多数业务场景下它是一个稳健、低成本、能让流程跑通的基线方案。新手容易陷入一个误区——总想找到完美处理缺失值的方法结果在预处理阶段耗费大量时间。我的原则是先快速建立baseline再逐步优化。1.1 先看数据的病在哪里缺失不是千篇一律的缺失值处理之前必须先弄清楚缺失机制。统计上分为三种完全随机缺失缺失与否和任何变量无关纯粹是数据采集时的意外。这种缺失用平均值填充是最安全的不会引入系统性偏差。随机缺失缺失与否与其他已观测变量有关。比如用户年龄字段缺失可能和用户是否注册时长有关。这种情况下按分组取均值效果更好。非随机缺失缺失与否和缺失值本身有关。比如收入越高的人越不愿意填收入。这种最麻烦平均值填充会明显引入偏差更合适的做法是建模预测缺失值或者干脆把是否缺失也作为一个特征放进去。我这次遇到的数据集经过简单相关性分析后发现缺失情况大致属于前两种混合。平均值填充作为基线是合理的。1.2 平均值、中位数、众数到底该用谁很多人拿到缺失值就统一填均值其实不同填充值适合不同类型的数据分布填充方式适用场景优点缺点平均值数值型特征分布近似正态或均匀简单、计算快、保留样本量对偏态分布和异常值敏感中位数数值型特征分布偏斜或存在极端值抗异常值能力强对后续模型来说信息量可能不足众数分类型特征简单直观类别分布极不均匀时可能引入偏差我这次处理的四个数值特征有两个分布近似正态一个右偏明显还有一个带明显的离散峰。按道理右偏那个应该用中位数填充但为了保持流程的一致性并验证基线效果我统一先用平均值填。实验结果也侧面证明了这个选择的影响——模型AUC达到了0.85以上但那个右偏特征的系数置信区间偏宽后来我改用分组均值填充后情况有所改善。1.3 平均值填充的适用边界与潜在风险平均值填充有一个隐藏的副作用压缩了特征的方差。填充进去的都是一样的值相当于把所有缺失样本在该特征上拉到了同一个位置这会人为降低特征的变化幅度从而影响模型对真实数据分布的刻画。处理方式是在填充之后额外增加一个缺失指示特征import pandas as pd # 对缺失值填充前先记录缺失标记 for col in [age, income, usage_days, last_order_amount]: df[col _was_missing] df[col].isnull().astype(int) df[col] df[col].fillna(df[col].mean())这个操作的意义在于机器学习模型本身没法知道哪些值是被填充的如果我们直接填一个固定的平均值进去模型会把这些样本在对应维度上视为完全一样。而记录缺失标记后模型就有机会学到这个字段缺失的用户可能具有某种倾向——比如流失风险更高。这种做法在实际业务中往往能带来微弱但稳定的提升。2. 逻辑回归不是回归先搞懂它凭什么能做分类逻辑回归这个名字经常把人绕进去它名义上带回归两个字但干的是分类的活。理解这一点才谈得上用好它。2.1 从线性回归到Sigmoid概率是怎么算出来的线性回归输出的是连续值范围从负无穷到正无穷没法直接当作概率。逻辑回归的思路是先按线性回归的方式算出加权求和再套一层Sigmoid函数把结果压缩到(0,1)区间。Sigmoid函数的形式是z w1*x1 w2*x2 ... b p 1 / (1 exp(-z))当 z 为正时p接近1z为负时p接近0。这里的p就可以解释为样本属于正类的概率。这个映射关系非常直观输入特征通过线性加权得到一个分数分数越高属于正类的概率越大而Sigmoid把这个分数变成了我们熟悉的概率尺度。2.2 决策边界与线性可分性逻辑回归擅长什么、不擅长什么逻辑回归的决策边界是线性的。在二维平面上体现出的一条直线在三维空间是一个平面在高维空间是一个超平面。这意味着如果数据本身就近似线性可分逻辑回归能给出很漂亮的分类效果而且训练速度极快。如果类别分布呈现非线性缠绕比如环形、月牙形等逻辑回归的表现会很挣扎此时你需要更复杂的模型或者先对特征做组合变换、核技巧。我在这次用户流失预测中特意排查了特征与标签的关系。流失用户和非流失用户在大部分特征维度上确实存在近似线性的分界比如使用天数越短、最近下单金额越低流失概率越高。这类场景就是逻辑回归的主场。2.3 损失函数与梯度下降训练时到底在优化什么逻辑回归的损失函数不是均方误差而是交叉熵损失L -[y*log(p) (1-y)*log(1-p)]为什么用交叉熵而不是均方误差核心原因是Sigmoid函数两端饱和梯度趋近于零如果使用均方误差梯度下降的速度会非常慢而交叉熵在预测错误时能产生较大的梯度帮助参数迅速修正。训练时通过梯度下降不断更新每个特征的权重w和偏置b。权重的绝对值大小反映了对应特征对分类结果的影响程度正负号反映影响方向。这就是逻辑回归作为可解释模型的价值所在。3. 从预处理到训练构建一条完整的分类流水线模型原理清楚了接下来是把流程串起来。这里每一个步骤的顺序都不能乱顺序错了模型性能会打折扣甚至出现数据泄漏导致结果虚高。3.1 数值型与分类型特征的处理顺序原始数据里通常混着数值特征和类别特征。类别特征需要编码这个步骤必须在填充之后进行或者说至少要在同一份数据上同步处理。我这次数据里有两个类别特征用户等级和渠道来源。用户等级有普通、白银、黄金、铂金四个档位。这里我用了哑变量编码df_encoded pd.get_dummies(df, columns[user_tier, channel], drop_firstTrue)不用drop_first的话线性模型内部会出现多重共线性问题逻辑回归的系数解读会变得混乱。3.2 训练集/测试集划分填充必须在划分之后这是整个流程里最重要、也最容易出错的一步。必须先切分训练集和测试集再分别在训练集和测试集上执行填充。为什么如果先对整个数据集用全体均值填充再切分训练集和测试集那么测试集的均值信息已经泄露到了训练过程中。测试集不再是一个未知的数据集而是提前知道了整体分布评估指标会虚高。正确做法是from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( df.drop(churn, axis1), df[churn], test_size0.2, random_state42, stratifydf[churn] ) # 分别在训练集和测试集上填充填充值来自训练集统计量 mean_values X_train.mean() X_train X_train.fillna(mean_values) X_test X_test.fillna(mean_values)注意一个容易被忽略的细节填充均值要用训练集的均值而不是测试集的均值。因为测试集永远模拟的是未来新数据我们只能假设它拥有和训练集相似的分布而不能直接使用它的统计量。3.3 标准化与逻辑回归的兼容性逻辑回归对特征的尺度是敏感的。如果一个特征以元为单位取值范围在0到几万另一个特征以年为单位取值范围在0到3那么权重的收敛速度和可解释性都会受影响。梯度下降时数值小的特征可能还没收敛数值大的特征已经在跳跃式前进了。所以我用StandardScaler做了标准化。标准化同样要先在训练集上fit再transform测试集from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这个步骤和填充的逻辑一脉相承fit永远在训练集上test集只做transform。这样才能保证测试集的评估结果是对未来数据的无偏估计。3.4 模型训练与超参数设置准备工作完成后训练模型本身反而非常简洁from sklearn.linear_model import LogisticRegression model LogisticRegression( C1.0, penaltyl2, solverlbfgs, max_iter1000, class_weightbalanced, random_state42 ) model.fit(X_train_scaled, y_train)几个参数的选择要说明一下C是正则化强度的倒数。C越小正则化越强系数越趋向于零。我一开始用默认值1.0发现验证集上F1勉强及格后来调小到0.5效果反而更稳说明数据中存在一定噪声。penaltyl2是默认的选择适合大多数场景如果你需要特征选择可以换成l1。max_iter1000是因为特征数量较多时默认的100次迭代可能出现不收敛警告。class_weightbalanced非常重要。流失用户占比大约20%属于类别不平衡场景如果不加这个参数模型会倾向于把所有样本预测为不流失准确率看着很高实则毫无用处。训练完成后模型系数最大的几个特征依次是最近下单金额使用天数注册时长方向也都符合业务直觉在线时长越短、下单金额越低的用户流失概率越大。4. 评估不是只看准确率分类模型的全面体检训练完成只是开始评估才是检验模型有没有真正学到东西的环节。我见过太多人只报一个accuracy就觉得自己完成了任务这在类别不平衡时几乎是自欺欺人。4.1 混淆矩阵从四个格子看全局先看混淆矩阵预测为流失 预测为非流失 实际流失 TP FN 实际非流失 FP TNTP真流失且预测为流失FN真流失但预测为非流失漏报FP真非流失但预测为流失误报TN真非流失且预测为非流失我这次测试集有6000条样本打印出来的混淆矩阵如下from sklearn.metrics import confusion_matrix tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() # 假设数值tn4120, fp380, fn580, tp920这组数字说明真正流失的1500人里我们只抓住了920人漏掉了580人而预测流失的1300人里有380人是误报。业务上漏报意味着该挽回的用户没来得及挽回误报意味着把不该打扰的用户给打扰了。两者代价不同需要结合业务权衡。4.2 精确率与召回率的权衡精确率 TP / (TP FP)预测为流失的样本里有多少真的流失了。高精确率意味着误报率低。召回率 TP / (TP FN)真实流失的样本里有多少被抓住了。高召回率意味着漏报率低。两者通常不可兼得。你想多抓流失用户就会提高召回率但代价是更多非流失用户被误伤精确率下降。所以需要用F1-score取它们的调和平均来评估综合表现。这次模型在默认阈值0.5下精确率约0.71召回率约0.61F1约0.66。作为基线已经可接受但仍然有明显的提升空间。后面我会谈到阈值调节。4.3 ROC曲线与AUC从排序的角度理解模型好坏ROC曲线和AUC的核心思想是评估模型的排序能力——把真正流失的用户排在不流失用户前面的概率。AUC0.5表示模型完全随机AUC1.0表示完美排序。我这次AUC达到0.86说明模型对流失风险的排序能力是靠谱的。注意AUC与阈值无关它衡量的是模型在所有可能阈值下的整体区分能力所以即使你后续调整分类阈值AUC不会变它衡量的是模型底子的好坏。4.4 阈值选择默认0.5不一定最优逻辑回归输出概率后默认做法是把大于0.5的判为正类。但0.5不一定适合你的业务场景。在流失预测场景中挽回一个高价值用户的收益远大于打扰一个低活跃用户的成本。所以我更倾向于把阈值调低一些比如0.35意味着只要流失概率超过35%就触发挽回动作。这样召回率会上升误报率也上升但整体业务ROI可能更高。实操时可以通过绘制P-R曲线或者遍历阈值表来找到最优切分点from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds precision_recall_curve(y_test, y_proba[:, 1]) # 找到F1最高的threshold f1_scores 2 * (precisions * recalls) / (precisions recalls 1e-9) best_threshold thresholds[f1_scores.argmax()] print(f最佳阈值为 {best_threshold:.3f})我最后选定的阈值是0.33在这个阈值下召回率提升到了0.78精确率小幅降到0.62但综合业务收益明显更优。5. 实操复盘平均值填充逻辑回归的坑与改进思路整套流程跑完后我重新审视了每一步有四个坑是我之前没预料到或曾经翻过车的这里逐一说明。5.1 平均值填充导致方差缩小的隐患前面提过平均值填充会压缩特征方差。这在逻辑回归中的具体体现是对某些特征被填充样本的系数贡献权重被稀释因为填充值缺乏区分度。如果缺失率超过20%还无脑用单一均值填充模型的AUC通常会有3%~5%的损伤。这时候可以引入随机噪声# 基于随机填充的改进方案 missing_std df[col].std() df[col] df[col].fillna(df[col].mean() np.random.normal(0, missing_std * 0.05, sizedf[col].isnull().sum()))或者更稳妥的做法按类别分组填充。比如按用户等级分组计算均值高级用户的年龄缺失值用高级用户群体的均值填充而不是全体用户的均值。在实际业务里这种做法的效果往往来得更直接。5.2 特征共线性对系数解释的影响逻辑回归的系数解释依赖特征之间相互独立。如果两个特征高度相关比如累计登录次数和累计在线时长它们会互相竞争解释力导致各自系数不稳定甚至出现一个正、一个负的反直觉结果。排查方法是用相关系数矩阵或VIF方差膨胀因子。统计上VIF超过10就要警惕我这次虽然没到这一步但在对系数做业务解读时发现登录次数的系数不显著进一步排查后发现它和使用天数的相关系数高达0.87。解决方式是在两个高相关特征中只保留解释性更强的一个或者用PCA降维如果目的是业务解释优先选择前者。5.3 从逻辑回归到更复杂模型什么时候该升级逻辑回归是强基线但不是终点。如果你发现以下现象就该考虑升级模型了特征中存在明显的非线性关系比如年龄和流失率呈U型分布年轻的流失老年的也流失中间的忠诚。交互效应显著比如高活跃且高金额与低活跃但高金额的流失模式完全不同。验证集指标始终无法通过调参突破某个瓶颈。升级路径可以有两条。一条是树模型方向随机森林和梯度提升树能自动处理非线性与缺失值但解释性弱一些。另一条是给逻辑回归加特征工程比如增加多项式特征、分箱操作让线性模型能捕捉非线性信息。我这次在完成逻辑回归后顺手跑了另外两个实验作为对比一个用随机森林一个在逻辑回归基础上加入了年龄分箱交互特征。结果随机森林AUC约0.88比逻辑回归高但逻辑回归的稳定性更好、复现更简单。在没有明确复杂需求时先用逻辑回归做基线永远是正确策略。5.4 交叉验证与模型稳定性检查单个训练集/测试集划分的结果存在偶然性尤其是数据量不大时。我习惯在调参阶段使用5折交叉验证确保模型性能不是靠运气好的划分方式得到的from sklearn.model_selection import cross_val_score scores cross_val_score(model, X_train_scaled, y_train, scoringroc_auc, cv5) print(f5折AUC均值: {scores.mean():.4f}, 标准差: {scores.std():.4f})这次交叉验证的AUC均值0.85标准差只有0.012说明模型在不同数据划分下表现稳定。标准差过大是特征噪声大或数据分布不稳定的信号需要通过特征选择或增加正则化来压制。最后再补充一个个人体会在做分类项目时模型本身往往只占30%的工作量数据质量、特征理解和评估设计才是决定性因素。平均值填充配合逻辑回归这套组合可能不够炫酷但它足够可靠、可解释、可复现适合绝大多数结构化数据的分类起点。先把这条基线链路跑扎实再去追复杂模型路会顺很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑