8000 条申请、三个真实特征,AUC 是 0.736。 加一列「贷后 90 天状态」,训练 AUC 冲到 0.939、测试 0.934。 但上线时这一列拿不到——AUC 立刻掉到 0.715,比不用它的 0.718 还低 0.003。 top10% 捕获率从 19% 掉到 18%:批出去的额度,抓到的坏客户比老老实实那版还少。
第十二章进入金融与农业。这两个行业看着毫不相干,却共用同一套建模纪律。 第一个纪律是数据治理——不是「数据多不多」,是「这些字段在那一刻真的存在吗」。
一、最贵的一个字段
风控模型里,决定成败的往往不是算法,而是特征在业务发生的那一刻是否可得。 「贷后 90 天状态」这个字段,只有审批通过、放款之后才存在。 但它躺在数据仓库里,和「收入」并排,看起来一样干净。 一列这样的字段混进训练集,模型会做一件很聪明也很致命的事: 把权重几乎全压在它身上,因为它的信号最强。 这一节的 Demo 就造这个局面。 违约率 35%,真实特征只有收入、负债比、历史逾期次数三个, 再按标签造一个「贷后状态」——违约的人给 45 天左右,没违约的给 6 天左右, 两批人有重叠,但分得很开。 然后对比两个特征集在同一批测试集上的表现,再把泄漏列整列换成训练集均值, 模拟「上线后拿不到」。
二、0.939 与 0.715 之间隔着一个字段
| 特征集 | 训练 AUC | 含泄漏测试 | 上线 AUC | top10% 捕获 |
|---|---|---|---|---|
| 三个真实特征 | 0.736 | 0.718 | 0.718 | 19% |
| 加上贷后状态 | 0.939 | 0.934 | 0.715 | 18% |
| 先看第二行:训练 0.939、测试 0.934。 | ||||
| 这两个数几乎一样,说明它不是过拟合——它在测试集上真的很准。 | ||||
| 比第一行高出 20 多个点,评审会上没人会怀疑这个模型。 | ||||
| 但「测试集上很准」和「上线后很准」是两件事。 | ||||
| 因为测试集也是从历史数据里随机切的,它同样带着那一列。 | ||||
| 把这一列换成训练集均值(相当于上线时它根本不存在),AUC 掉到 0.715。 | ||||
| 注意它比第一行的 0.718 还低 0.003。 | ||||
| 为什么不用它反而更好?因为模型把学习的力气用错了地方。它花在「怎么从贷后状态反推违约」上, | ||||
| 剩下三个特征没被好好学。 | ||||
| 这不是「噪声多了」,是「注意力偏了」。 | ||||
| 泄漏字段的伤害不是加噪声,是把有用的信号挤掉。 |
三、业务后果比指标后果更难看
AUC 差 0.003 听起来无关紧要,但看最后一列: top10% 捕获率从 19% 掉到 18%。 这个指标的口径是:按模型分数排序,批给前 10% 的人,这批人里能抓到多少真正的坏客户。 它是唯一一个能直接换算成钱的指标——额度是有限的。 19% 掉到 18% 意味着:同样的额度规模,多放出去一批会违约的人。 而这一切的起因,是有人往特征表里多加了一列。 数据泄漏最阴的地方就在这里:它在离线指标上给你奖励。 你越早发现它,损失越小;一旦它通过评审上了线,你要等到第一次真实坏账爆发才会知道。 而坏账爆发往往滞后三到六个月——那时候钱已经放出去很久了。
四、落地:三道闸门
① 问一句「这个字段在决策那一刻存在吗」。 贷后状态、结清日期、催收记录、退保时间——统统不属于申请时点。 这条问句能挡掉八成的泄漏,而且不需要跑任何代码。 ② 看单特征重要度。 某一个特征的重要度远超其他所有特征之和时,先怀疑泄漏,再怀疑自己发现了真理。 正常的业务特征很少能单独贡献一半以上的解释力。 ③ 用时间切分,不用随机切分。 随机切分会让同一条时间线前后的信息互相渗透——同一个客户在训练集和测试集里各出现一次, 模型就「见过」他了。 时间切分(用前 70% 训练、后 30% 测试)才能复现真实的上线环境。 还有一条更硬的办法:拿上线头一个月的真实表现做终审。 离线指标再漂亮,也要等一次「影子模式」跑完才敢真放量。 影子模式的意思是:模型照常打分,但不参与决策,只看它打的分准不准。 最后一句:数据治理不是清洗脏数据,是确认每个字段的「时间归属」。 一行数据干不干净,看格式;一个字段能不能用,看它什么时候才存在。
五、动手 Demo:数据泄漏:加了「贷后 90 天状态」,训练 AUC 0.93 上线只剩 0.72
造 8000 条贷款申请,三个真实特征(收入、负债比、历史逾期次数)加噪声生成标签, 再按标签造一个「贷后 90 天状态」列——违约的偏大、未违约的偏小,两批有重叠。 前 70% 训练、后 30% 测试,比较「三特征」与「三特征加泄漏列」的 AUC; 再把泄漏列整列填成训练集均值,模拟上线后拿不到;最后算 top10% 捕获率,看业务后果。
# 贷后状态:违约的人天数大,没违约的小,但两批有重叠
post = np.where(y > 0.5, rg.normal(45, 25, n),
rg.normal(6, 12, n))
X_ok = np.column_stack([inc, dti, dpd]) # 三个真实特征
X_leak = np.column_stack([inc, dti, dpd, post]) # 多一列「未来」
# 上线时 post 拿不到,整列填训练集均值
X_live = np.column_stack([inc, dti, dpd,
np.full(n, post.mean())])
auc(X_ok, y), auc(X_leak, y), auc(X_live, y)
实跑输出:
--------------------------------------------------------------------
数据泄漏:加了「贷后 90 天状态」,训练 AUC 0.93 上线只剩 0.72
--------------------------------------------------------------------
样本 8000 条申请,违约率 35%
真实特征 收入、负债比、历史逾期次数
泄漏特征 贷后 90 天状态(只有审批完才知道)
切分 前 70% 训练,后 30% 测试
特征集 训练 AUC 含泄漏测试 上线 AUC top10% 捕获
------------------------------------------------------------
三个真实特征 0.736 0.718 0.718 19%
加上贷后状态 0.939 0.934 0.715 18%
**先看第二行:训练 AUC 0.939、测试 AUC 0.934。**
**这个模型在纸面上几乎完美——比第一行高了 21.62 个点。**
**但把「贷后 90 天状态」这一列换成训练集均值(模拟上线时拿不到),AUC 立刻掉到 0.715。**
**比只用三个真实特征的 0.718 还低。**
**为什么反而更低?因为模型把权重全压在那一个特征上,剩下三个特征没被好好学。**
**这不是「噪声多了」,是「学习的力气用错了地方」。**
**top10%% 捕获率那一列最能说明业务后果。**
**第一行 19%,第二行上线之后 18%。**
**也就是说,批 10% 的额度,能抓到的违约客户比老老实实做模型的那版还少。**
**这就是数据泄漏最阴的地方:它在离线指标上给你奖励。**
**你越早发现它,损失越小。**
**如果它一路通过评审上了线,你要等到第一次真实坏账爆发才会知道。**
实务上的识别办法有三条:
**① 问「这个字段在审批那一刻存在吗」。**贷后状态、结清日期、催收记录——统统不属于申请时点。
**② 看单特征重要度。** 某一个特征的重要度远超其他所有之和时,先怀疑泄漏,再怀疑自己发现了真理。
**③ 用「时间切分」而不是「随机切分」。**
随机切分会让同一条时间线前后的信息互相渗透,**时间切分才能复现真实的上线环境。**
要说明:数据是我**按三特征加噪声造的**,泄漏字段也是我按标签造出来的。
**真实业务里的泄漏更隐蔽——比如「某月所有申请人都通过了」,那可能只是那个月改了政策。**
**但「泄漏让离线指标虚高、上线就崩」这条规律是真的。**
这张表最值得盯着的是第二行的前两列:0.939 和 0.934。 它们几乎相等,所以「过拟合」这个常见的怀疑在这里不成立——模型在没见过的数据上一样准。 问题在于「没见过的数据」也带着那一列。 随机切分切开了行,没切开时间。 第三列 0.715 才是它上线后的真实水平,而且它比 0.718 还低——因为权重被抢走了。 这一条可以当成判据:如果加了一个特征之后 AUC 猛涨 20 多个点,不要庆祝, 先去查这个特征什么时候才存在。 要说明:数据是我按三个特征加噪声造的,泄漏字段也是按标签造的。 真实业务里的泄漏更隐蔽——比如「某月所有申请人都通过了」,那可能只是那个月改了政策。 但「泄漏让离线指标虚高、上线就崩」这条规律是真的。
30 秒实验
- 把噪声标准差从 2.6 调到 1.0,再看第一行 AUC。 真实特征变强之后,泄漏带来的「虚高」会缩小——因为模型不必再去抓那一列。
- 把「贷后状态」换成「客户性别」再跑一次。 AUC 几乎不动——不是所有能拿到的字段都有用,可得的字段里也有大量噪声。
- 把切分改成时间切分(前 70% 的月份训练、后 30% 测试),但保留泄漏列。 AUC 还是会虚高,因为泄漏和标签是同一个时间点的信息——这说明时间切分挡不住真的泄漏, 只能挡住「同客户重复」。
常见误区
- 「训练和测试 AUC 一样,所以没有过拟合、可以上线」。 两个数一样只能说明切分没泄露行,不能说明字段没泄露时间。
- 「AUC 掉 0.003 是噪声,忽略即可」。top10% 捕获率同时掉了 1 个点,那是钱。
- 「模型自己会学会忽略无用特征」。信号越强的特征,模型越会往上压权重,哪怕它上线后不可得。
自测三问(点开对答案)
1. 为什么加上泄漏特征之后,上线 AUC 反而比不用它更低? —— 模型把权重压在泄漏列上,三个真实特征没被充分学习; 上线后泄漏列失效,剩下的特征质量比单独训练时更差。 2. 训练 0.939 和测试 0.934 都很高,为什么不能说明模型可用? —— 因为测试集也是从同一份历史数据随机切的,同样带着泄漏列; 它只能证明模型在「有未来信息」的条件下准。 3. 为什么说 top10% 捕获率比 AUC 更能反映业务后果? —— 额度是有限资源,捕获率直接对应「同样额度能减少多少坏账」; AUC 是全排序的统计量,不落到具体资源分配上。数据干净了,模型上线了。下一个问题是「它什么时候开始变坏」——而且有两种坏法,一种吵、一种静。
- ☐ (本节完,下一节 12.1.2 模型部署与监控)