🏠 总目录📚 本教程 14 · 泄漏的七种来源
📑 本页目录(点开跳转)

14 · 数据泄漏的七种来源

70 分钟 | ⭐⭐ 离线好得离谱的时候,先查这里


🎯 一句话

数据泄漏 = 训练时用到了「预测那一刻根本拿不到」的信息。 它不会报错、不会让 loss 不降、不会让任何一个测试失败 —— 它只会让你的离线指标变好看,然后在上线那天把账一次性还回来。


🔬 一、先看一组实跑出来的数字

下面这些不是估计值,是真跑出来的(numpy + sklearn,代码在本章末尾和第 20 章)。 同一份数据、同一个模型,只改「怎么切、什么时候做预处理」:

   把七种泄漏【同时】犯一遍:

     离线 5 折 CV 的 AUC .......... 0.977   ← 你会截图发群里
     诚实评估的 AUC ............... 0.573   ← 上线后的真实水平

     落差 = 0.404          三个随机种子跑出来:0.406 / 0.403 / 0.391
   再把七种拆开,各自单独犯一次(都是同一套数据、同一个模型):

   泄漏类型              有泄漏      无泄漏     落差
   ───────────────────────────────────────────────
   ① 时间穿越(随机切分) 0.810  →   0.682    0.128
   ② 目标编码没做 OOF     0.854  →   0.707    0.147   ⭐ 这是 CV 分
   ③ 预处理先于切分(SMOTE)0.984  →   0.849    0.135
   ④ 分组泄漏(同用户)   0.706  →   0.564    0.142
   ⑤ 重复样本跨集         0.745  →   0.605    0.140
   ⑥ 代理变量             0.970  →   0.686    0.284   💀 最大
   ⑦ 外部数据带未来信息    —— 症状同 ①,但更难查

注意这组数字的形状:单独一种泄漏大约值 0.13–0.15 AUC, 但七种叠起来是 0.40 —— 它们会互相放大,不是相加。 这就是为什么「离线 0.97、线上 0.6」在真实项目里并不罕见: 通常不是犯了一个大错,而是同时犯了五六个小错。


🧭 二、判断是不是泄漏,只需要问三个问题

   对每一个特征、每一条流程,问:

   ① 时间:这个值在【预测那一刻】就已经存在了吗?
        —— 不是"数据库里有",是"那一刻已经产生并且能查到"

   ② 身份:这个值的计算,用到过【被预测的这一行的标签】吗?
        —— 包括间接用到(编码、填充、采样、筛选)⭐

   ③ 边界:训练集和测试集之间,有没有【同一个东西】跨过去?
        —— 同一个人、同一次会话、同一条被复制的记录、同一天

🔑 第 ② 条最容易被漏掉,因为它常常藏在「预处理」里StandardScaler 见过测试集、fillna(median) 用的是全量中位数、 SMOTE 拿测试集的正例插值出了训练样本 —— 这些都不叫特征工程,这些叫「让标签从后门溜进训练集」。


⏰ 三、① 时间穿越

最常见,也最贵。

内容
症状 随机切分 CV 很稳(0.81),换成按时间切分立刻掉一大截(0.68)⭐ 这个对比本身就是诊断
特征重要性榜首是 dt / create_time / 更新时间 这类字段
上线后第一周还行,越往后越差(因为你训练时"见过"的那些日子过完了)
怎么验证 跑两次:随机 KFold vs 按时间切。差 > 0.03 AUC 就当作有时间泄漏
把每个特征单独和 day 做相关性排序,看有没有「只在训练期内可解释」的字段
怎么修 评测一律用按时间前后切;删掉任何形式的时间 id 类特征
所有聚合特征加一句「截止到 T 时刻」,并在代码里真的实现它

上面那个实跑例子里有个细节值得单独说:

   随机切分 AUC = 0.810
   按时间切分   = 0.682
   按时间切 + 【把 day 这一列删掉】 = 0.685   ⭐ 几乎没变

day 那一列在诚实评估里一点用都没有(0.682 vs 0.685), 但在随机切分里它值 0.13 个 AUC。 这就是时间泄漏最阴的地方:这个特征的"价值"完全来自切分方式,不来自数据。 你在特征重要性图上会看到它排第一,然后你会以为它很重要。

最隐蔽的一种时间穿越:口径写得对,实现写错了。

   需求文档:  "用户近 30 天的下单次数"
   SQL 实现:  SELECT COUNT(*) FROM orders WHERE user_id = ?
               AND dt BETWEEN '2025-01-01' AND '2025-01-31'

   💀 这条 SQL 对【每一行样本】用的都是同一个固定窗口,
      于是 1 月 3 日那条样本的特征里,
      包含了 1 月 3 日到 1 月 31 日发生的事。

   ⭐ 正确写法必须让窗口跟着样本时间走:
      AND o.dt BETWEEN date_sub(s.event_time, 30) AND s.event_time
                                                     ↑ 这个上界是命门

🎯 四、② 目标编码没做嵌套 CV

高基数类别(城市 id、商户 id、SKU)做 target encoding 时, 如果用整个训练集算编码、再在同一份数据上做 CV, 每一行的标签都参与了它自己那个类别的均值。

实跑(2000 个取值、平均每类 4 行、这个类别和标签完全无关):

做法 训练集 5 折 CV AUC 真正 holdout AUC
朴素目标编码 0.854 💀 0.577
OOF 目标编码 0.707 0.726

💀 第一行是所有虚假 CV 分数的教科书样本: 一个完全没有信息的随机 id,被朴素编码之后, 把 CV 从 0.71 抬到了 0.85,而 holdout 只有 0.577 —— 它抬高的全是幻觉,而且还顺手把真特征挤掉了。

第二行有个反直觉但极重要的现象: 修好泄漏之后 CV 从 0.854 掉到 0.707, 但 holdout 从 0.577 涨到 0.726「修泄漏会让你的线下分数变难看」是正常的、也是必然的。 如果你的团队按 CV 分数考核,那这个修复动作在流程上就是不可能发生的 —— 这是一个组织问题,不是技术问题。

内容
症状 高基数类别特征在重要性榜上排第一,但它的业务含义你说不清
类别取值越碎,"效果"越好(平均每类 3 行时最夸张)⭐ 这是判定信号
CV 和 holdout 差 > 0.1
怎么验证 把该类别列打乱重排再跑一次。分数没掉多少 = 它本来就没信息,掉的分是泄漏
怎么修 out-of-fold 编码:编码器只在当前折以外的数据上 fit
低频类别做平滑((sum + prior*k) / (cnt + k)k 取 10–50)
编码器必须进 Pipeline不能在切分之前算好

🧪 五、③ 预处理在切分之前做

这一类里最贵的三个:标准化、缺失填充、重采样(SMOTE)

   标准化   —— 泄漏了测试集的均值和方差       损失通常很小(0.00x)
   缺失填充 —— 泄漏了测试集的中位数/众数      损失中等
   SMOTE    —— 💀 泄漏了测试集正例的【位置】  损失巨大

实跑(正例率 8.3%,随机森林):

做法 AUC
先对全量 SMOTE,再随机切分 0.984 💀
先切分,只对训练集 SMOTE,在真实测试集上评估 0.849

💀 为什么 SMOTE 是这三个里最毒的: SMOTE 的合成样本是两个真实少数类样本之间的插值点。 先合成再切分,等于把测试集正例的坐标,以"合成样本"的名义抄进了训练集 —— 模型不需要学规律,只要记住"我见过这个点附近"。 而且它的最终指标是在一个被合成样本污染的测试集上算的,双重虚高。

内容
症状 不平衡数据上 AUC > 0.97,但上线后精确率惨不忍睹
混淆矩阵好得不真实:极低的假阳性 + 极低的假阴性同时出现
怎么验证 检查代码顺序:train_test_split 这一行在不在所有 fit/fit_resample 之前
Pipeline 重跑一次,两个分数一对比就知道了
怎么修 一条铁律:切分是第一步,永远是第一步。
所有有状态的变换(scaler / imputer / encoder / 采样)全部塞进 Pipeline
测试集永远保持原始分布,绝不重采样 —— 重采样只是训练技巧,不是评测口径

👥 六、④ 分组泄漏:同一个用户跨集

同一个人、同一个病例、同一台设备的多条记录,被随机切分拆散到了训练集和测试集。

实跑(900 个用户 × 12 条记录,用户级随机效应很强):

切分方式 AUC
随机切分(行级) 0.706
GroupKFold 按用户切 0.564

0.706 里有 0.14 是"认人",不是"预测行为"。 模型学到的是「这个用户的另外 8 条记录标签是什么」, 而线上遇到的是新用户,它一条也没见过。

内容
症状 线上对老用户表现尚可、对新用户明显更差 ⭐ 这是最强的信号
用户级特征(注册渠道、设备型号、常驻城市)重要性异常高
训练集和测试集的用户 id 交集很大
怎么验证 一行代码:len(set(uid_train) & set(uid_test))期望是 0
GroupKFold 重跑,掉 > 0.03 就确认
怎么修 按业务实体切分(用户 / 病人 / 门店 / 设备),不按行切
⚠️ 组的选择要看线上会遇到谁:如果线上大多是新用户,就必须按用户切;如果线上永远是老用户的新行为,那按时间切才对

⚠️ 医疗和风控里这个坑最贵:同一个病人的多张影像、同一个人的多笔交易。 一篇论文报 AUC 0.95,换成按病人切之后是 0.78 —— 这类"复现失败"绝大多数是它。


👯 七、⑤ 重复样本跨集

数据回填、多次导出、上游重试、宽表 join 出现一对多 —— 重复记录是常态。

实跑(40% 的行被复制了一份,1-NN 分类器):

做法 AUC
带重复直接随机切分 0.745
先去重再切分 0.605
内容
症状 复杂模型(深树、KNN、大网络)比简单模型好得离谱 —— 因为只有它们记得住
训练集行数比业务口径的"应有条数"多
数据量翻倍时指标不涨反跌(去重后才涨)
怎么验证 对全部特征列做哈希,统计重复行数;再统计跨训练/测试的重复对数
⚠️ 还要查近似重复:文本样本改了个标点、图片改了个尺寸
怎么修 切分前去重;去重必须先于切分,顺序反了等于没做
文本用 MinHash / SimHash,图片用感知哈希,不能只查完全相同
⭐ 去重之后指标会掉,掉下去的那部分本来就不属于你第 6 章

🕵️ 八、⑥ 代理变量:一个只有事后才存在的字段

这是落差最大的一种(0.284),也是最容易被业务同学一眼看穿的一种。

实跑:给数据加一列「退款金额」,只有流失用户里 92% 的人非零。

特征集 AUC
退款金额 0.970 💀
剔除后 0.686
   常见的代理变量(每一个都在真实项目里出现过):

   预测流失      ← 「退款金额」「客服工单数」「账号注销申请时间」
   预测欺诈      ← 「风控备注」「人工审核结论」「冻结状态」⭐ 结论字段最毒
   预测疾病      ← 「用药记录」「转诊科室」「住院天数」
   预测违约      ← 「催收次数」「展期申请」
   预测点击      ← 「曝光时长」「页面停留」(这些是点击【之后】才有的)

💀 最毒的一类是「人工审核结论」这种字段: 它不是"和标签相关",它几乎就是标签本身的另一个副本。 而且它在数据库里的名字往往看不出来 —— status_codeflag_3remark_type

内容
症状 AUC > 0.95 且只靠一两个特征就能达到(去掉它就崩)⭐⭐
单特征 AUC 就有 0.9+
怎么验证 ⭐ 算每个特征的单变量 AUC,排序。任何单特征 AUC > 0.85 都要人工过一遍
对 Top-3 特征问业务:「这个字段是在我们要预测的事情发生之前写入的,还是之后?
怎么修 删掉;或者查清它的写入时刻,只用预测时刻之前的版本
⭐ 在数据契约(第 3 章)里给每个字段登记 produced_at,这是根治的办法

🛑 读到这里可以停 —— 前半章讲完了(约 25 分钟)。 后半章还有:⑦ 外部数据带来的未来信息 · 症状速查:「离线好得离谱」到底长什么样 · 一次真实的事故复盘 · 一套上线前的泄漏体检(可以直接抄) · 把这一章变成流程,而不是知识 回来的时候不用重读,直接从下一节接着看就行。


🌍 九、⑦ 外部数据带来的未来信息

买来的第三方数据、公开数据集、爬来的补充特征 —— 它们的时间戳通常是"最后更新时间",不是"当时的值"

   典型形态:

   · 企业工商数据       ——「经营状态」是【今天】的状态,
                          你却把它当成两年前那笔贷款申请时的状态 💀
   · 第三方信用分       ——供应商每月刷新,历史版本不保留
   · 城市 POI / 房价    ——只有最新一版
   · 爬来的商品评分      ——评分是累计到今天的
   · 预训练模型的语料    ——⭐ 你的评测集可能已经在它的训练语料里了
内容
症状 加了外部数据之后离线指标大涨(+0.05 以上),线上却几乎没变 ⭐
越久远的样本,外部特征"越准" —— 因为它们泄漏的未来更多
怎么验证 按样本时间分段看提升:真信息的提升应该各段接近;泄漏的提升会在老样本上更大
问供应商要一个字段:这一行的快照时间。要不到就当作有泄漏
怎么修 只用能提供历史快照的外部数据源;自己按天存快照
⚠️ 用预训练模型做评测时,先查评测集有没有进过它的语料第 13 章第四节讲了怎么查:去重 / n-gram 重叠 / 改写测试 / 新鲜题对照,⚠️ 拿不到训练语料时只剩后两个)

🩺 十、症状速查:「离线好得离谱」到底长什么样

这张表是本章最该被记住的东西。 你不需要记住七种类型,只需要认得这些形状:

你看到的现象 最先怀疑
AUC > 0.95,而这个业务问题本来很难 ⭐⭐ 代理变量或时间穿越
去掉某一个特征,AUC 从 0.97 掉到 0.70 💀 代理变量,几乎可以确诊
随机 CV 稳,按时间切掉一大截 时间穿越
CV 0.85,holdout 0.58 目标编码 / 预处理先于切分
对老用户好、对新用户差 分组泄漏
复杂模型比简单模型好得不成比例 重复样本
不平衡数据上假阳假阴同时极低 SMOTE 先于切分
加了外部数据后离线大涨、线上不涨 外部数据带未来
重要性榜首是一个你说不清含义的字段 泄漏(哪一种都有可能)
上线第一周尚可、之后逐周变差 时间穿越

⭐⭐ 一条可以当口诀的判据「模型好得让你想截图」和「模型好得让你想再验一遍」,应该是同一个反应。 在数据这一关,指标突然变好首先是一个待排查的异常第 0 章 那张表的第二行)。


💀 十一、一次真实的事故复盘

   系统:某消费信贷的首逾预测模型(预测放款后首期是否逾期)
   数据:申请单 62 万条,跨 26 个月
   团队:3 个算法 + 1 个数据工程

发生了什么

时间 事件
第 1 周 基线模型 KS 0.32、AUC 0.74,符合行业水平
第 3 周 接入第三方「多头借贷」数据,AUC 跳到 0.89、KS 0.61 ⭐ 全组振奋
第 4 周 又加了商户维度的目标编码(商户 id 约 4 万个),AUC 0.93
第 5 周 评审通过,理由:「提升幅度巨大且 5 折 CV 非常稳定,折间标准差只有 0.004」
第 8 周 上线,10% 流量
第 12 周 首批样本到期,线上 AUC 0.71 —— 比上线前的老模型还低 0.01

为什么没被发现

   ① 5 折 CV 是【随机】切的 —— 26 个月的数据被打散了       泄漏①
   ② 目标编码在切分之前算好,4 万个商户平均每个 15 单     泄漏②
   ③ 第三方多头数据是【当前快照】,供应商不提供历史版本    泄漏⑦
      → 一个 2023 年的申请人,配的是 2025 年的多头记录
      → 已经逾期的人在 2025 年的多头次数当然更高  💀

   ⭐ 折间标准差 0.004 反而是【泄漏的证据】而不是稳定性的证据:
      泄漏的信息在每一折里都同样存在,所以每一折都同样虚高。
      "非常稳定"在这里的意思是"每一折都错得一样多"。

代价

   放款金额         这 4 个月按新模型放出去    2.7 亿
   实际首逾率       预期 1.9%  →  实际 3.4%
   多出来的坏账     约 4050 万 × 回收率折算后 ≈ 2900 万
   人力             3 人 × 12 周 白做,加 5 周返工
   ⭐ 从上线到发现  28 天(等首期到期,这是硬约束)
   ⭐ 从建模到发现  9 周   ← 真正该压缩的是这一段

该补什么

缺失 补上之后
CV 用随机切分 改成按申请时间切;新老两种切法的差值进模型报告,差 > 0.03 不许评审
目标编码在 Pipeline 外 全部有状态变换进 Pipeline代码评审时专门看 split 那一行的位置
外部数据没有快照时间 采购合同里加一条「必须提供 snapshot_date」;⭐ 拿不到就只在"当前预测"用,绝不进历史训练集
没有单特征 AUC 体检 上线前必跑;单特征 AUC > 0.85 的字段要写一份说明
用 CV 分数汇报成果 改用按时间的滚动回测,且必须报「最后一段」的分数

💀 这个事故里最值得记住的一句「5 折 CV 折间标准差只有 0.004」被当成了模型可靠的证据。 而实际上,当所有折都共享同一份泄漏时,一致性恰恰是泄漏的指纹。 ⭐ 判断稳定性要看的是不同切分方式之间的差异,不是同一种切分内部的方差。


🔧 十二、一套上线前的泄漏体检(可以直接抄)

第 1 段:单特征 AUC 排序 —— 抓代理变量,10 行,最划算的一段代码

import numpy as np
from sklearn.metrics import roc_auc_score

def single_feature_auc(X, y, names, warn=0.85):
    """每个特征单独算 AUC。⭐ 抓代理变量最快的办法"""
    out = []
    for j, nm in enumerate(names):
        v = np.asarray(X[:, j], dtype=float)
        ok = ~np.isnan(v)
        if ok.sum() < 50 or len(np.unique(y[ok])) < 2:
            continue
        a = roc_auc_score(y[ok], v[ok])
        a = max(a, 1 - a)                      # ⭐ 方向无所谓,看的是"分得开吗"
        out.append((nm, round(a, 3), "🚨 疑似泄漏" if a > warn else ""))
    return sorted(out, key=lambda r: -r[1])

# ⭐ 判读:任何单特征 AUC > 0.85 都要人工过一遍它的写入时刻

第 2 段:切分体检 —— 三个边界一次查完

import numpy as np, hashlib

def split_audit(X_tr, X_te, t_tr=None, t_te=None, g_tr=None, g_te=None):
    """一次查完:时间重叠 / 组重叠 / 重复样本跨集"""
    rep = {}
    if t_tr is not None:                       # ⭐ 时间边界:训练集最晚 < 测试集最早
        rep["时间重叠"] = float(np.max(t_tr) - np.min(t_te))
    if g_tr is not None:                       # ⭐ 组边界:期望恰好是 0
        rep["跨集组数"] = len(set(np.asarray(g_tr).tolist())
                              & set(np.asarray(g_te).tolist()))
    h = lambda A: {hashlib.md5(np.ascontiguousarray(r).tobytes()).hexdigest()
                   for r in np.asarray(A, dtype=float)}
    rep["跨集重复行"] = len(h(X_tr) & h(X_te))  # ⭐ 期望也是 0
    return rep

# 判读:三个数字里任何一个 > 0,就先别看指标了

第 3 段:正确的 out-of-fold 目标编码(带平滑)

import numpy as np
from sklearn.model_selection import KFold

def oof_target_encode(cat_tr, y_tr, cat_te, k=20, n_splits=5, seed=0):
    """⭐ 训练集用 OOF 值,测试集用全训练集的映射;k 是平滑强度"""
    cat_tr, y_tr = np.asarray(cat_tr), np.asarray(y_tr, dtype=float)
    prior = y_tr.mean()

    def fit_map(c, y):
        m = {}
        for v in np.unique(c):
            s = y[c == v]
            m[v] = (s.sum() + prior * k) / (len(s) + k)   # ⭐ 低频类别被拉回先验
        return m

    oof = np.full(len(cat_tr), prior)
    for a, b in KFold(n_splits, shuffle=True, random_state=seed).split(cat_tr):
        mp = fit_map(cat_tr[a], y_tr[a])                 # ⭐⭐ 只用【折外】数据 fit
        oof[b] = [mp.get(v, prior) for v in cat_tr[b]]
    mp_all = fit_map(cat_tr, y_tr)
    enc_te = np.array([mp_all.get(v, prior) for v in np.asarray(cat_te)])
    return oof, enc_te

# ⚠️ 修好之后 CV 分数【一定会掉】。掉下去的那部分本来就不是你的。

🧱 十三、把这一章变成流程,而不是知识

泄漏是「知道了也照样会犯」的那类问题,因为它藏在代码顺序和 SQL 边界里。 唯一有效的办法是把检查变成会失败的东西

   上线前必跑的四条门禁(任何一条不过就拦住):

   ① 切分体检三个数字全为 0(时间重叠 / 跨集组 / 跨集重复行)
   ② 单特征 AUC 榜首 < 0.85,否则必须附一份字段说明
   ③ 随机切分 AUC 和 按时间切分 AUC 的差 < 0.03            ⭐⭐ 最有价值的一条
   ④ 所有有状态变换都在 Pipeline 里(代码评审人肉确认)

   ⭐ ③ 之所以最有价值:它不需要你知道泄漏在哪,
      它只需要两次实验,就能回答"我的评测方式本身可不可信"。

一条会省下很多时间的经验每次指标出现「大幅、意外、说不清来源」的提升,先花半小时跑一遍这四条门禁。 代价是半小时;不跑的代价,看上面那个 2900 万。


🔗 这一章连到哪里

去哪 为什么
Kaggle 23 数据泄露与外部数据 竞赛视角的同一件事:那边泄漏是可以"利用"的(榜单奖励它),这边是纯粹的事故。对比着读能看清两种语境的差别
模型上线之后 02 离线好不等于线上好 「离线好线上差」的完整原因清单,泄漏只是其中一条;那边还有分布差异、反馈回路、口径不一致
模型上线之后 04 训练推理一致性 ⭐ 「特征穿越」在上线侧的样子。本章讲怎么在训练前发现,那章讲怎么在上线前逐字段比对
ML 基础 05 评估与过拟合 切分和交叉验证的基本功。泄漏本质上是评测协议的问题,不是模型问题

✅ 检查点

  1. 数据泄漏的一句话定义是什么?判断泄漏的三个问题分别问什么?
  2. 七种泄漏各自单独犯一次,AUC 落差大约是多少?七种一起犯呢?这说明了什么?
  3. 时间穿越那个实验里,day 这一列在诚实评估里值多少?在随机切分里值多少?这说明特征重要性图会告诉你什么错误信息?
  4. 朴素目标编码在那个实验里把 CV 抬到了多少、holdout 是多少?修好之后 CV 和 holdout 分别变成多少?这个变化方向为什么很重要?
  5. 标准化、缺失填充、SMOTE 三种"预处理先于切分"里,哪一种损失最大?为什么?
  6. 分组泄漏最强的线上症状是什么?怎么用一行代码验证?
  7. 代理变量怎么快速筛出来?阈值定在多少?举三个真实场景的代理变量例子。
  8. 外部数据泄漏怎么验证?为什么"老样本上提升更大"是泄漏的信号?
  9. 信贷那个事故里,「5 折 CV 折间标准差只有 0.004」为什么反而是泄漏的证据?代价是多少?发现用了多久?
  10. 上线前四条门禁是什么?哪一条最有价值、为什么?
👀 答案
  1. 训练时用到了「预测那一刻根本拿不到」的信息。三个问题:①时间——这个值在预测那一刻已经产生并能查到吗;②身份——它的计算用到过被预测这一行的标签吗(包括间接:编码、填充、采样、筛选);③边界——训练集和测试集之间有没有同一个东西跨过去(同一个人/会话/被复制的记录/同一天)。
  2. 单独一种约 0.13–0.15 AUC(①0.810→0.682、②0.854→0.707、③0.984→0.849、④0.706→0.564、⑤0.745→0.605、⑥0.970→0.686),七种一起犯是 0.977 → 0.573,落差 0.404。说明泄漏会互相放大不是相加——「离线 0.97 线上 0.6」通常不是犯了一个大错,而是同时犯了五六个小错
  3. 诚实评估里 day0.003(0.682 vs 去掉它的 0.685,几乎为零);随机切分里它值 0.128。说明这个特征的"价值"完全来自切分方式而不是数据,但它会在特征重要性图上排第一,让你以为它很重要。
  4. 朴素编码 CV 0.854 / holdout 0.577;OOF 编码 CV 0.707 / holdout 0.726。方向很重要因为:修好泄漏会让 CV 分数掉(0.854→0.707)而真实分数涨(0.577→0.726)。⭐ 如果团队按 CV 分数考核,这个修复动作在流程上就是不可能发生的——这是组织问题不是技术问题
  5. SMOTE 最大(0.984 vs 0.849)。因为合成样本是两个真实少数类样本之间的插值点,先合成再切分等于把测试集正例的坐标以"合成样本"的名义抄进训练集;而且最终指标还是在被合成样本污染的测试集上算的,双重虚高。铁律:切分永远是第一步,测试集永远保持原始分布、绝不重采样。
  6. 对老用户表现尚可、对新用户明显更差。验证:len(set(uid_train) & set(uid_test))期望是 0;或换 GroupKFold 重跑,掉超过 0.03 即确认(实验里 0.706 → 0.564)。
  7. 每个特征的单变量 AUC 并排序,任何单特征 AUC > 0.85 都要人工过一遍。例子:预测流失用「退款金额/客服工单数/注销申请时间」、预测欺诈用「人工审核结论/冻结状态」(结论字段最毒,它几乎是标签的副本)、预测点击用「页面停留时长」(这是点击之后才有的)。
  8. 按样本时间分段看提升:真信息的提升各段接近,泄漏的提升在老样本上更大——因为老样本距离"当前快照"更远,快照里包含的未来更多。同时向供应商索要 snapshot_date要不到就当作有泄漏
  9. 因为泄漏的信息在每一折里都同样存在,所以每一折都同样虚高——"非常稳定"在这里的意思是"每一折都错得一样多"。⭐ 判断稳定性要看不同切分方式之间的差异,不是同一种切分内部的方差。代价:4 个月放款 2.7 亿,首逾率 1.9% → 3.4%,坏账约 2900 万,3 人 12 周白做 + 5 周返工。从上线到发现 28 天(等首期到期,是硬约束),从建模到发现 9 周——真正该压缩的是后面这一段。
  10. ①切分体检三个数字全为 0 ②单特征 AUC 榜首 < 0.85 ③随机切分与按时间切分的 AUC 差 < 0.03 ④所有有状态变换都在 Pipeline 里。③最有价值:它不需要你知道泄漏在哪,只需要两次实验就能回答「我的评测方式本身可不可信」。

🛑 可以停在这里

走神救援

⭐⭐泄漏 = 训练时用到了「预测那一刻拿不到」的信息。它不报错、不让 loss 不降、不让任何测试失败,只让离线指标变好看,然后上线那天一次性还账。三个判据:①时间(那一刻这个值产生了吗)②身份(它的计算用过这一行的标签吗,包括间接的编码/填充/采样/筛选)③边界(有没有同一个东西跨集)。⭐实跑数字:单独一种泄漏值 0.13–0.15 AUC七种一起犯是 0.977 → 0.573,落差 0.404——泄漏会互相放大不是相加,所以"离线 0.97 线上 0.6"通常不是一个大错而是五六个小错。七种:①时间穿越(随机切 0.810 / 按时间切 0.682;⭐day 那列在诚实评估里只值 0.003——它的"价值"全来自切分方式,却会在重要性图上排第一;最隐蔽形态是 SQL 用了固定窗口而不是跟着样本时间走的窗口)②目标编码没做 OOF(一个完全无信息的随机 id 把 CV 从 0.707 抬到 0.854,holdout 只有 0.577;⭐⭐修好后 CV 掉到 0.707 而 holdout 涨到 0.726——「修泄漏会让线下分数变难看」是必然的,如果团队按 CV 考核,这个修复在流程上就不可能发生;验证法:把该列打乱重跑,分数没掉多少说明它本来就没信息)③预处理先于切分(标准化损失很小、填充中等、💀SMOTE 最毒 0.984 vs 0.849,因为合成样本是真实少数类之间的插值点,等于把测试集正例的坐标抄进训练集,而且指标还是在被污染的测试集上算的;⭐铁律:切分永远是第一步,测试集绝不重采样)④分组泄漏(随机切 0.706 / 按用户切 0.564,⭐最强症状是"对老用户还行、对新用户明显差";一行验证 len(set(uid_tr)&set(uid_te)) 期望为 0;医疗论文"复现失败"大多是它)⑤重复样本跨集(0.745 vs 0.605;⭐症状是复杂模型比简单模型好得不成比例——因为只有它们记得住;必须先去重再切分,还要查近似重复 MinHash/感知哈希)⑥代理变量落差最大 0.970 vs 0.686;退款金额/客服工单/人工审核结论——💀结论字段几乎就是标签的副本,而且名字看不出来叫 status_codeflag_3;⭐筛法:算每个特征的单变量 AUC,>0.85 就人工过一遍写入时刻)⑦外部数据带未来(第三方数据的时间戳是"最后更新时间"不是"当时的值";⭐验证法:按样本时间分段看提升,泄漏的提升会在老样本上更大;要不到 snapshot_date 就当作有泄漏)。⭐⭐症状速查最该记的几条AUC>0.95 而问题本来很难 → 代理变量或时间穿越去掉一个特征 AUC 从 0.97 掉到 0.70 → 代理变量可以确诊随机 CV 稳但按时间切掉一大截 → 时间穿越对老用户好对新用户差 → 分组泄漏重要性榜首是一个你说不清含义的字段 → 泄漏。💀信贷事故:接了第三方多头数据 AUC 0.74→0.89,再加商户目标编码到 0.93,评审理由是"5 折 CV 折间标准差只有 0.004",上线 4 个月后线上 AUC 0.71,比老模型还低。⭐⭐那个 0.004 恰恰是泄漏的指纹——泄漏的信息在每一折里同样存在,所以"非常稳定"的意思是"每一折都错得一样多";判断稳定性要看不同切分方式之间的差异,不是同一种切分内部的方差。代价:放款 2.7 亿、首逾 1.9%→3.4%、坏账约 2900 万、3 人 12 周白做;上线到发现 28 天(等首期到期,硬约束),建模到发现 9 周——该压缩的是后面这段。⭐四条上线门禁:①切分体检三个数字(时间重叠/跨集组/跨集重复行)全为 0 ②单特征 AUC 榜首 <0.85 ③⭐⭐随机切分与按时间切分的 AUC 差 <0.03 ④有状态变换全在 Pipeline 里。③最值钱,因为它不需要你知道泄漏在哪,两次实验就能回答"我的评测方式本身可不可信"每次出现大幅、意外、说不清来源的提升,先花半小时跑这四条——不跑的代价看那 2900 万。

下一节 👉 15-采样偏差与幸存者偏差.md

打卡记录保存在你的浏览器里,首页能看到总进度