🧠 第十二章 金融与农业:驱动智能决策与产业升级
📄 一句话概括

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 秒实验

  1. 把噪声标准差从 2.6 调到 1.0,再看第一行 AUC。 真实特征变强之后,泄漏带来的「虚高」会缩小——因为模型不必再去抓那一列。
  2. 把「贷后状态」换成「客户性别」再跑一次。 AUC 几乎不动——不是所有能拿到的字段都有用,可得的字段里也有大量噪声。
  3. 把切分改成时间切分(前 70% 的月份训练、后 30% 测试),但保留泄漏列。 AUC 还是会虚高,因为泄漏和标签是同一个时间点的信息——这说明时间切分挡不住真的泄漏, 只能挡住「同客户重复」。

常见误区

  1. 「训练和测试 AUC 一样,所以没有过拟合、可以上线」。 两个数一样只能说明切分没泄露行,不能说明字段没泄露时间。
  2. 「AUC 掉 0.003 是噪声,忽略即可」。top10% 捕获率同时掉了 1 个点,那是钱。
  3. 「模型自己会学会忽略无用特征」。信号越强的特征,模型越会往上压权重,哪怕它上线后不可得。
自测三问(点开对答案) 1. 为什么加上泄漏特征之后,上线 AUC 反而比不用它更低? —— 模型把权重压在泄漏列上,三个真实特征没被充分学习; 上线后泄漏列失效,剩下的特征质量比单独训练时更差。 2. 训练 0.939 和测试 0.934 都很高,为什么不能说明模型可用? —— 因为测试集也是从同一份历史数据随机切的,同样带着泄漏列; 它只能证明模型在「有未来信息」的条件下准。 3. 为什么说 top10% 捕获率比 AUC 更能反映业务后果? —— 额度是有限资源,捕获率直接对应「同样额度能减少多少坏账」; AUC 是全排序的统计量,不落到具体资源分配上。

数据干净了,模型上线了。下一个问题是「它什么时候开始变坏」——而且有两种坏法,一种吵、一种静。

相关笔记