资讯动态

类别不平衡二分类实战:从评价指标到重采样与阈值移动

发布时间:2026/10/16 8:42:08 来源:尧图企业网站定制
类别不平衡这个问题我一开始真没拿它当回事。直到有一次在模拟项目X里做会员流失预测训练集中正样本只占4%我用默认参数跑了一个逻辑回归准确率96.8%当时还挺高兴。可把混淆矩阵打印出来一看真正流失的用户里模型只找回了不到10%。那一刻我才意识到数据建模里最不值得信任的数字就是这种被多数类堆出来的准确率。这篇笔记是我在完整复盘那个项目之后整理出来的。我尽量不绕弯子直接讲清楚类别不平衡会干扰哪些环节、评价指标怎么选、数据侧和算法侧分别有哪些处理手段以及模型上线后还要注意什么。如果你正在做二分类而且少数类的占比很低比如低于10%那这篇文章应该对你有帮助。读完你可以直接拿里面的思路去检查自己的模型不用重走我踩过的坑。1. 类别不平衡问题到底在哪一环开始捣乱1.1 先定义“不平衡”比例低到多少才需要专门处理类别不平衡不是二分类特有的多分类里也会遇到但二分类最典型。如果正负样本比例接近比如1:1或者1:2通常不需要做额外处理。少数类占比低于10%的时候就要开始留意了低于1%属于极端不平衡处理不当的话模型即便在验证集上拥有很高的AUC落地时也可能完全没法用。为什么会这样因为绝大多数模型的学习目标是让整体损失最小化。以逻辑回归为例它的损失函数是所有样本损失的平均值。当负样本占总体96%时负样本的梯度贡献主导了参数更新正样本即使全部预测错误也只会拉扯很小的权重。树模型也一样分裂时计算信息增益多数类覆盖的样本多它天然更关心怎样把多数类分对。这不是模型傻而是优化目标和业务目标出现了偏差。可以打个比方如果一个班级里100个人只有1个人近视你让一个统计模型预测谁近视它只要说“全班都不近视”准确率就是99%。但如果目的是把近视的人筛选出来去做视力复查这个模型毫无价值。类别不平衡的本质就是训练信号被大量多数类样本稀释了。1.2 一组可以用来跟练的实验数据长什么样我在模拟项目X里用的是一份会员行为数据规模约30万条其中流失用户约1.2万占比4%。字段包括近30天登录次数、平均订单间隔、优惠券核销数、投诉次数、最近一次购买距今天数、会员等级等。目标变量是未来30天内用户是否流失。听起来不算太复杂但这种数据结构在现实中非常常见。多数用户稳定活跃、近期有购买行为少数用户开始失联、投诉增多最后悄悄流失。模型要做的就是从一堆“看起来还健康”的用户里把即将流失的那一小撮人抓出来。这里想强调一个初学者容易忽视的点处理这类数据时最好不要随机打乱后划分训练集和验证集。流失预测是有时间含义的模型是用来预测未来不是预测过去。我先按时间顺序切分用前80%的月份做训练后20%的月份做验证这样得到的指标才反映真实业务场景。另外同一个用户的行为记录不应该同时出现在训练集和验证集里否则模型很容易“背答案”。后续我会在验证策略那章再详细展开。1.3 直接跑一个基线模型你会看到什么第一次实验我没有做任何特殊处理直接把原始训练集丢给逻辑回归和随机森林。逻辑回归在验证集上的准确率96.7%随机森林96.8%看着都很漂亮。但是换成混淆矩阵就不一样了验证集中1200个真实流失用户随机森林只抓到了109个召回率9%。后面一查几乎所有模型都在预测“不流失”。这个现象非常典型。在样本占比4%的情况下模型只要把所有样本都判为多数类准确率就能到96%。它根本不需要学习任何流失特征因为学习流失特征反而会在少数样本上犯错拉低整体收益。所以你的基线模型分数越高未必代表越有用可能只说明你的数据不平衡程度很严重。看到高准确率先别高兴去翻一下少数类的召回率再决定要不要进入下一步处理。2. 评价指标才是最大的坑准确率如何骗人2.1 先把混淆矩阵的四格看懂很多教程喜欢直接讲指标公式但我觉得先从混淆矩阵入手更直观。假设二分类问题中正类是我们要找的流失用户负类是正常用户。预测结果有四种可能真阳性TP、假阳性FP、真阴性TN、假阴性FN。这四格几乎可以推导出一切常用指标。比如准确率等于(TPTN)/(TPFPTNFN)。在4%正样本的数据里TP0、FP0、TN28800、FN1200准确率是97.6%。公式本身没有错但它把占大头的TN正确排除的正常用户也算进分子于是模型即便一败涂地准确率依然非常体面。所以我的习惯是每次实验第一件事先打印四类格子而不是只看一个综合指标。混淆矩阵会告诉你模型到底在哪一侧栽了是抓不住正样本FN高还是误报太多FP高。后续所有调优动作都必须基于其中某一侧的痛点来展开否则就是在盲调。2.2 哪些指标值得盯一个速查表结合大家常用的指标我整理了一个对照表适合类别不平衡场景下快速定位该看什么指标含义在不平衡数据中的作用精确率 PrecisionTP/(TPFP)预测为正类的样本中真正类所占比例回答“模型叫出来的用户里有多少真的流失”召回率 RecallTP/(TPFN)真实正类中被成功预测的比例回答“真实流失用户里模型抓住了多少”F1精确率与召回率的调和平均用一个数值兼顾精确率与召回率适合成本不明确时PR-AUC精确率-召回率曲线下面积随少数类表现变差而显著下降贴近业务痛点AUC-ROCROC曲线下面积衡量排序能力但对类别分布不敏感容易被高估混淆矩阵TP/FP/TN/FN四格用于诊断具体错误类型是最基础的信息源这里特别想提醒AUC的局限。AUC只看正负样本分数的排序关系不关注概率绝对值。换句话说就算模型对所有正样本都给0.49、负样本给0.48只要排序正确AUC也可以很高。可在实际业务中总分0.49根本没法作为决策阈值用。所以我在后面的项目里逐渐把AUC当作辅助指标主指标换成PR-AUC和阈值约束下的召回率。2.3 用业务目标反推指标权重指标不是选得越全越好而是要和你能承受的成本绑在一起。在模拟项目X里业务方的要求很直接“尽量多找出可能流失的用户但客服人力有限打出去的100个电话里至少要有30个是真正要流失的。”翻译成指标语言就是召回率尽量高同时精确率不低于30%。这个约束一旦定下来整个建模目标就清晰了。我不再纠结“F1最大”这种模糊目标而是在验证集上搜索满足精确率条件的最优阈值。简单代码可以这样写import numpy as np from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_val, proba) mask precision 0.3 if mask.any(): best_idx np.argmax(recall[mask]) best_threshold thresholds[mask][best_idx] print(fbest threshold {best_threshold:.3f})这段代码跑出来的最优阈值在0.18附近对应召回率约61%、精确率约32%。业务方看到这个结果才觉得模型真的能落地因为此前默认0.5阈值时的召回率只有9%。同样的模型、同样的验证集只是把决策边界从0.5改成0.18业务价值就完全不同。这就是为什么要理解指标背后的业务含义。3. 从数据侧修不平衡重采样不是无脑过采样3.1 下采样先把训练分布拉平但别丢弃信息处理类别不平衡数据侧最常见的两招是下采样和过采样。先说说下采样也叫欠采样。它的逻辑很朴素既然多数类太多那就从多数类里随机抽出一部分让训练集正负样本数量接近1:1。实现上可以借助imblearn库几行代码就能搞定from imblearn.under_sampling import RandomUnderSampler rus RandomUnderSampler(random_state42) X_res, y_res rus.fit_resample(X_train, y_train)随机下采样最大的优点是把训练分布拉平后模型能充分看到正样本训练也更快。缺点是随机丢弃大量多数类样本可能会把一些重要的负样本模式扔掉。比如在流失预测里很多“最近投诉次数很多但还在活跃”的用户可能是最容易被误判成流失的负样本。随机下采样如果把这些样本丢了模型在验证集上的误报就会变高。缓解办法也很直接多采几次每个模型用不同的随机种子欠采样最后把结果集成起来。这就是EasyEnsemble的核心思想。实际操作时我大概做10轮随机欠采样每轮训练一个树模型最后对概率分数取平均。models [] for seed in range(10): rus RandomUnderSampler(random_stateseed) X_res, y_res rus.fit_resample(X_train, y_train) clf RandomForestClassifier(n_estimators100, random_stateseed) clf.fit(X_res, y_res) models.append(clf) proba np.mean([m.predict_proba(X_val)[:, 1] for m in models], axis0)需要注意欠采样只作用于训练集。验证集和测试集必须保持原始分布否则你评估出来的召回率和精确率会严重失真。这一点我最初也踩过后面会专门讲。3.2 SMOTE与合成样本边界比想象中容易出问题下采样牺牲数据量很多人不满意于是想用人工合成样本。SMOTE就是最出名的一种。它的原理是对每个少数类样本在特征空间中找到若干近邻样本然后在两者连线上随机插入新样本。这样既不丢失数据又能增加少数类样本量。from imblearn.over_sampling import SMOTE smote SMOTE(random_state42) X_res, y_res smote.fit_resample(X_train, y_train)听起来很美好但我在模拟项目X里试用下来效果并不好。SMOTE合成的样本在验证集上反而让F1下降了。原因也很有代表性流失数据里很多特征是稀疏的比如登录次数、优惠券核销数存在大量0值。SMOTE用欧氏距离找近邻时很容易在0值密集的区域造出“仿佛活跃又仿佛不活跃”的中间样本这些合成样本反而成了噪声让模型学出一套错误的边界。如果确实想用SMOTE建议优先尝试Borderline-SMOTE或者SMOTE-ENN、SMOTE-Tomek这类先合成、再对重叠样本做清洗的组合方法。我后来测试SMOTETomek Links效果就比单纯SMOTE好一点但相比下采样集成仍然没有明显优势。我的经验是当特征数量多、稀疏程度高、或者正样本本身只有几百条时不要迷信SMOTE。合成数据和真实数据不同它没有“业务事实”支撑只会让模型在某些区域过度自信。3.3 组合策略的对比不要指望单次重采样解决一切为了看清楚不同重采样策略的影响我把几种常见做法放到同一个验证集上做了比较。验证集始终保持原始4%的分布下表是我在模拟项目X中得到的一组结果策略AUCPR-AUCF1备注无处理/随机森林0.710.160.18准确率虚高召回极低随机欠采样Bagging0.820.310.42多次采样摊平信息损耗SMOTE0.780.270.36合成噪声导致指标下降SMOTETomek Links0.800.290.38清洗重叠区域后略有提升随机欠采样XGBoost0.840.360.47树模型权重调整后最优数据会因为业务场景不同而有差异但趋势很典型重采样的作用是改变训练分布并不等于模型效果一定变好。必须配合阈值调整和合适的算法。我在这个项目里最终采用的是“随机欠采样 XGBoost”再往下就会用到类权重和阈值移动这两部分才是真正把重采样效果放大成业务价值的环节。4. 算法侧也有牌可打类权重与阈值移动4.1 class_weight的真实效果与代价如果不想破坏训练集也可以从算法层面给少数类更多“话语权”。最经典的做法就是class_weight。在逻辑回归或树模型里给少数类的损失乘上一个较大的权重相当于人为放大小样本错误的代价。from sklearn.linear_model import LogisticRegression lr LogisticRegression(class_weightbalanced)class_weightbalanced会自动根据类别频率分配权重少数类样本的权重高一些多数类权重低一些。这样做不会丢弃样本也保留了完整信息。我在模拟项目X里加了class_weight后AUC从0.71提升到0.80看起来很好。但这里有个坑模型输出的概率整体被抬高了。原本全部样本的平均流失概率可能只有4%加了权重后预测概率平均值可能到10%甚至更高。如果还用0.5作为阈值会出现大量误报。原因很简单你用加权损失训练出的模型学习的并不是真实事件概率而是一个被扭曲过的风险分数。class_weight对模型的贡献是改善排序能力帮助把“真流失”排到“不流失”前面但它并不能直接告诉你应该选哪个概率作为分界。4.2 阈值移动把0.5换成业务需要的阈值这是整个类别不平衡处理链路里最简单也最容易见效的一步。很多人在训练时做了各种处理最后却默认用0.5判断等于前面全白干。正确的做法是用验证集重新计算不同阈值下的精确率、召回率和F1再根据业务目标选阈值。from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_val, proba) f1 2 * precision * recall / (precision recall 1e-9) best_idx np.argmax(f1[:-1]) # 最后一个点对应无穷大阈值需要去掉 best_threshold thresholds[best_idx] print(fbest threshold {best_threshold:.3f}, F1 {f1[best_idx]:.3f})如果业务损失不明确可以直接看F1最大的阈值。但更严谨的做法是把成本量化。假设漏掉一个流失用户带来的损失是100元误触达一个正常用户的成本是5元。就可以先算所有样本的漏报率和误报率再算每个阈值下的期望损失最后选总损失最小的阈值。下面是我当时跑出来的一个示例阈值召回率精确率期望成本0.512%55%5.6元/人0.245%38%3.1元/人0.168%23%2.8元/人0.0582%12%4.9元/人成本计算公式其实不复杂期望成本 漏报率 × 正样本占比 × 漏报损失 误报率 × 负样本占比 × 误报损失。按这个逻辑最优阈值落在0.1附近。阈值从0.5下移到0.1召回率从12%升到68%业务效果完全不一样。4.3 集成与scale_pos_weight算法侧的组合拳在树模型里除了class_weightXGBoost和LightGBM还提供了scale_pos_weight参数。很多新手会把它误当成“调大一点就能提高召回率”的旋钮其实它背后的含义是正负样本权重比。常见做法是设为负样本数除以正样本数。import xgboost as xgb model xgb.XGBClassifier( scale_pos_weightsum(y_train 0) / sum(y_train 1) )在我的模拟项目X里随机欠采样结合XGBoost和scale_pos_weight已经比单纯重采样效果好很多。原因在于梯度提升树在每一轮迭代中会自动聚焦之前分错的样本而scale_pos_weight进一步放大了正样本的梯度让树更倾向于分裂出少数类区域。不过要记住权重给得太大也会过拟合。常见做法是把scale_pos_weight设为负正比然后在这个范围附近做网格搜索比如试0.5倍、1倍、2倍。集成思路也一样BalancedRandomForest本质上是在每棵树训练时做平衡采样多个平衡树再聚合天然适合极端不平衡场景。算法侧和重采样不是互斥的它们可以组合使用但所有组合的结果都要回到验证集上用业务指标来验收。5. 工程落地时绕不开的细节验证策略与模型校准5.1 验证集划分分层采样不够要做时间或分组隔离数据建模做久了会发现很多模型在离线评估时表现不错上线后却立刻失效其中一大原因就是验证集划分没有模拟真实预测场景。类别不平衡问题里这就更致命了。如果同一用户的行为记录同时出现在训练集和验证集模型会学到很多“记忆式”规律召回率看起来很高但换一批新用户就原形毕露。所以在模拟项目X里我坚持按用户ID做分组切割。如果用交叉验证就用GroupKFold而非普通KFold。代码很简单逻辑却非常重要from sklearn.model_selection import GroupKFold gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groupsuser_id): # train on train_idx, validate on val_idx ...另一个要点是保持验证集原始分布。我见过有同学对全量数据做SMOTE后再划分验证集里正样本比例也被硬生生拉高到30%最后F1高得吓人一到线上就崩。记住重采样、过采样、SMOTE、邻域清洗全都只允许作用在训练集。验证集是“未来的新数据”它必须维持真实比例这样评估才有意义。5.2 概率校准让模型输出可以作为决策依据模型得分并不等于真实概率尤其在做过重采样或class_weight之后输出的概率值会被整体扭曲。如果下游只需要排序比如按分数从高到低打电话那校准没那么必要。但如果下游要做预期收益计算、成本估算或者要把概率交给业务方当作“流失风险百分比”那就必须做概率校准。sklearn里提供现成工具from sklearn.calibration import CalibratedClassifierCV cal_model CalibratedClassifierCV(model, methodisotonic, cv3) cal_model.fit(X_train_cal, y_train_cal) cal_proba cal_model.predict_proba(X_val)[:, 1]校准方法主要分两种Platt缩放适合模型曲线较平滑的情况等渗回归更灵活但样本量太小时容易过拟合。我一般先用Platt如果校准曲线明显非线性再换等渗。注意校准需要独立数据不要用训练集校准再用训练集评估否则会出现“概率很准但实际不靠谱”的假象。校准后的概率应该比校准前更接近真实比例。比如训练集正样本4%校准后验证集所有样本的平均预测概率应该也会落在4%附近。这时候前面选的0.1阈值才真正具有业务解释模型告诉你一个用户未来30天流失风险超过10%就进入客服召回名单。5.3 上线后监控和迭代分布会漂移模型上线不是结束而是监控循环的开始。尤其类别不平衡模型线上正样本率是漂移的第一信号。比如你现在训练时正样本占比4%三个月后因为市场环境变化真实流失率涨到8%那么模型的预测概率会整体上浮之前阈值选在0.1可能不再最优。我的习惯是每天记录三组数字线上正样本率、模型预测概率的均值、以及抽样标注的精确率和召回率。一周看一次趋势如果发现概率均值持续走高就要重新校准或重训。特征分布也值得关注常用PSI指标衡量。经验阈值大致是单个特征PSI低于0.1说明稳定0.1到0.25之间需要关注超过0.25说明已明显漂移。这些监控看起来和建模无关但恰恰是类别不平衡项目能否稳住的关键。因为不平衡比例本身就不是一成不变的。今天你辛苦找到的阈值明天可能因为一次活动、一个大版本更新就失效。所以建模笔记最后一定要补一句留好数据管道做好监控定期重训比追求某一时刻的最高分更重要。整套做下来我自己最深的体会是类别不平衡问题的核心其实不是某个算法有多神奇而是你有没有先把业务损失定义清楚。模型要抓的是哪部分样本漏掉和误伤的代价分别是什么这两个问题想明白了指标选择、阈值移动、校准这些操作才会有的放矢。最后再分享一个小技巧无论选择哪种重采样、类权重还是合成样本方案所有数据加工都只能发生在训练集内部。验证集和测试集始终要维持原始分布。我刚入行时就是对这个细节掉以轻心拿做过采样的数据做评估结果离线F1很高上线后一塌糊涂。现在我把这句话贴在工位上了每次建模前都会先念一遍。希望你不用再踩这个坑。

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

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

免费获取报价 →
↑