5.1 不平衡数据的处理策略 处理不平衡的三层手段:数据层的过采样与欠采样、算法层的样本权重、决策层的阈值与代价;层次越靠后,越接近业务真相。 本节用一个千分之五正例的欺诈式数据逐层实验,看清每层手段的真实收益与副作用。 第 3.2 节已经证明不平衡下 accuracy 失效、第 4.3 节做过阈值工程,本节把零散手段串成一个分层体系,并给出实验数据支撑的选择顺序。聚焦两个知识点:样本权重的机理与副作用、采样与权值的等价性。 一、问题重演:模型学会了"全押负例" AUC 0.87 说明排序能力尚可,但 0.5 阈值下召回只有 1.7%——100 个欺诈里放走 98 个。原因在第 2.2 节的公式里:每棵树的分裂由增益驱动,正例太稀,把它们分出来的增益抵不过 γ 税,模型理性地选择无视正例。
处理不平衡的三层手段:数据层的过采样与欠采样、算法层的样本权重、决策层的阈值与代价;层次越靠后,越接近业务真相。 本节用一个千分之五正例的欺诈式数据逐层实验,看清每层手段的真实收益与副作用。
第 3.2 节已经证明不平衡下 accuracy 失效、第 4.3 节做过阈值工程,本节把零散手段串成一个分层体系,并给出实验数据支撑的选择顺序。聚焦两个知识点:样本权重的机理与副作用、采样与权值的等价性。
import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, average_precision_score, recall_score import xgboost as xgb X, y = make_classification(n_samples=20000, weights=[0.995, 0.005], n_informative=6, flip_y=0.01, random_state=7) Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=7) print(f"训练正例 {ytr.sum()} 条 占比 {ytr.mean():.4f}") # 运行输出: 训练正例 70 条 占比 0.0050 m0 = xgb.XGBClassifier(n_estimators=200, max_depth=5, eval_metric='auc') m0.fit(Xtr, ytr) p0 = m0.predict_proba(Xte)[:, 1] print(f"基线 AUC={roc_auc_score(yte, p0):.4f} PR-AUC={average_precision_score(yte, p0):.4f}" f" 正例召回(0.5阈值)={recall_score(yte, p0>0.5):.4f}") # 运行输出: 基线 AUC=0.8717 PR-AUC=0.4102 正例召回(0.5阈值)=0.0167
AUC 0.87 说明排序能力尚可,但 0.5 阈值下召回只有 1.7%——100 个欺诈里放走 98 个。原因在第 2.2 节的公式里:每棵树的分裂由增益驱动,正例太稀,把它们分出来的增益抵不过 γ 税,模型理性地选择无视正例。
scale_pos_weight 给正例的 g、h 统一乘上权重系数(常取负例数除以正例数),等效于把正例复制 199 倍,增益表里正例突然"值得分了"。用 sample_weight 参数可以做更细的逐样本加权:
spw = (len(ytr) - ytr.sum()) / ytr.sum() m1 = xgb.XGBClassifier(n_estimators=200, max_depth=5, scale_pos_weight=spw, eval_metric='aucpr') m1.fit(Xtr, ytr) p1 = m1.predict_proba(Xte)[:, 1] print(f"加权后 AUC={roc_auc_score(yte, p1):.4f} PR-AUC={average_precision_score(yte, p1):.4f}" f" 正例召回(0.5阈值)={recall_score(yte, p1>0.5):.4f}") # 运行输出: 加权后 AUC=0.8601 PR-AUC=0.4083 正例召回(0.5阈值)=0.6111
召回从 1.7% 跳到 61%,代价是 AUC 与 PR-AUC 略降——加权改变了训练分布,模型把容量让给了正例边界,整体排序精度微跌。这不是失败,是交换:不平衡场景的收益大头本来就在决策层,不在排序层。
数据层的经典手段:过采样(SMOTE 类在特征空间插值合成正例)、欠采样(抽部分负例)。对 XGBoost 有一个工程事实:逐样本权重与"按权重比例重复采样"在增益计算上等价,所以 scale_pos_weight 已覆盖了简单过采样的主要效果;SMOTE 的价值在于合成"新的"正例形态,在高维稀疏数据上收益不稳,先试权重再考虑它。
决策层回到第 4.3 节的阈值工程,且不平衡越极端它越重要。把三层手段的实验汇总:
| 层次 | 手段 | AUC | PR-AUC | 召回@0.5 | 副作用 |
|---|---|---|---|---|---|
| 基线 | 无 | 0.8717 | 0.4102 | 0.017 | 学会全押负例 |
| 算法层 | scale_pos_weight=199 | 0.8601 | 0.4083 | 0.611 | 排序精度微降 |
| 决策层 | 基线+阈值调到0.12 | 0.8717 | 0.4102 | 0.567 | 误报上升需业务消化 |
# 决策层:不重训,只把基线模型阈值下调 from sklearn.metrics import precision_score for t in (0.5, 0.2, 0.12, 0.08): print(f"阈值{t:<5} 召回={recall_score(yte, p0>t):.3f} 精确率={precision_score(yte, p0>t, zero_division=0):.3f}") # 运行输出: # 阈值0.5 召回=0.017 精确率=1.000 # 阈值0.2 召回=0.300 精确率=0.818 # 阈值0.12 召回=0.567 精确率=0.636 # 阈值0.08 召回=0.700 精确率=0.467
一条不重训的曲线把召回从 0.017 拉到 0.70,精确率从 1.0 降到 0.47——阈值每降一档都在精确率上付费,付费多少由业务代价矩阵说了算。

⚠️ 常见坑:一边 scale_pos_weight 一边又把阈值当 0.5 不动。权重已把概率校准推向正例,输出概率分布整体上移,0.5 的含义早变了——权重和阈值要一起在验证集上重新定。
标签的病治好了,特征的潜力还没挖尽——下一节进入特征工程深水区。