🏠 总目录📚 本教程 20 · 实战与挑战项目
📑 本页目录(点开跳转)

20 · 实战与挑战项目

项目 1–2 天 / 挑战 4–6 天 | ⭐ 这一套必须动手才有用


🎯 一句话

前十九章讲的每一条,都要等你亲手把一份干净数据弄脏、再亲手查出来,才会变成直觉。 所以这四个项目的产出不是模型,而是 —— 一本你自己写的「我注入了什么」的真值台账,和一份「我查出了几种」的成绩单。


🧰 开工前:一个能反复弄脏的沙盘

和《模型上线之后》那套沙盘的区别在于:那边注入的是"上线之后发生的事",这边注入的是"数据本身的毛病"。 共同点是同一条纪律 —— 你必须知道真值,否则你没法给自己判卷。

import numpy as np

rng = np.random.default_rng(0)

def make_clean(n=50_000):
    """一份干净数据。⭐ 关键不是数据本身,是你【知道每一列该长什么样】"""
    return {
        "user_id": np.arange(1, n + 1),
        "age":     rng.integers(18, 80, n),
        "amount":  np.round(rng.lognormal(6, .8, n), 2),
        "city":    rng.choice(["北京", "上海", "广州", "深圳", "杭州"], n),
        "ts":      np.datetime64("2026-01-01") +
                   rng.integers(0, 180 * 86400, n).astype("timedelta64[s]"),
    }

def contaminate(d, kinds, rate=0.02, seed=1):
    """污染器:注入若干种脏,同时返回【真值台账】—— 这本账是整个项目的判卷依据"""
    r = np.random.default_rng(seed)
    d = {k: v.copy() for k, v in d.items()}
    n, truth = len(d["user_id"]), {}
    pick = lambda: r.choice(n, int(n * rate), replace=False)
    if "单位不一致" in kinds:                       # ⭐ 一部分金额从"元"变成"分"
        i = pick(); d["amount"] = d["amount"].astype(float); d["amount"][i] *= 100
        truth["单位不一致"] = set(i.tolist())
    if "不可能值" in kinds:
        i = pick(); d["age"] = d["age"].astype(float); d["age"][i] = 200
        truth["不可能值"] = set(i.tolist())
    if "空值伪装" in kinds:                         # ⭐ 不是 None,是字符串"未知"
        i = pick(); d["city"] = d["city"].astype(object); d["city"][i] = "未知"
        truth["空值伪装"] = set(i.tolist())
    return d, truth

💀 这里有一个必须先说的规矩污染器和检测器不能共用任何常量。 如果检测器直接 import 了污染器里那份"未知/NULL/-999"的哨兵字典, 你测的不是检测能力,是复制粘贴能力。 ⭐ 正确做法:两边分开写,检测器那份哨兵字典你要自己从头列一遍 —— 你会立刻发现自己漏了好几个。


🥉 项目一:亲手弄脏一份干净数据,再自己查出来(1–2 天)

目标:把第 4 章那十种形态一个个注入,再写一个检测器,看能查出几种。

检测器的骨架(自己往下加):

import numpy as np

SENTINEL = {"", "NULL", "null", "N/A", "-", "未知", "无", "-999", "0000-00-00"}

def detect(d):
    hits = {}
    a = np.asarray(d["age"], float)
    hits["不可能值"] = set(np.where((a < 0) | (a > 120))[0].tolist())      # 业务范围断言
    c = np.asarray(d["city"], object)
    hits["空值伪装"] = set(np.where(np.isin(c, list(SENTINEL)))[0].tolist())
    m = np.asarray(d["amount"], float)
    q1, q3 = np.percentile(m, [25, 75])
    hits["单位不一致"] = set(np.where(m > q3 + 50 * (q3 - q1))[0].tolist())  # ⭐ 量级跳变
    return hits

def score(truth, hits):
    for k, t in truth.items():                    # ⭐⭐ 召回和精确必须一起报
        h = hits.get(k, set()); tp = len(t & h)
        print(k, f"召回={tp/len(t):.2f} 精确={tp/max(len(h), 1):.2f}")

📏 参考数字(自己跑出来差太多就回去查)

形态 靠什么查 参考召回
类型漂移(数字存成字符串) dtype + 可转换率 0.99
编码错乱 / 双重编码 字符集白名单 0.95
类别值漂移(冒出新枚举) 枚举白名单 0.95
截断(字符串卡在 255) 长度分布的尖峰 0.90
不可能值(年龄 200) 业务范围断言 1.00
空值伪装"未知" / -999 哨兵字典 + 高频值 1.00(字典全的话)
时间格式混用 / 时区 解析成功率 + 小时分布 0.80
单位不一致(元 / 分) 量级跳变、双峰 0.71 ⚠️
近似重复(同一人两条) MinHash / 分块比对 0.60
跨列关联不一致(省市对不上) 跨列规则表 0.30 💀

✅ 通关判据(带数字,可以自己判卷)

判据 参考值
十种里查出几种 ≥ 7 种召回 > 0.5
误报率 ⭐⭐ 没被污染的对照集上跑一遍,被误标的行 < 1%
单位不一致的召回 0.6–0.8(到不了 1.0 是正常的,见下)
跨列关联的召回 < 0.4,并能说清为什么
台账 每一种形态都记了行号,能逐条对账

这个项目最有价值的是 T5,不是 T3。 "单位不一致"永远查不干净,因为一笔真实的 8,800 元订单和一笔被乘了 100 的 88 元订单, 在数值上完全无法区分 —— 你需要的不是更好的统计量,是契约里的单位字段第 3 章)。 ⭐⭐ "哪些脏靠看数据查不出来"这份清单,比"我查出了 8 种"这个成绩有用得多。

⚠️ 最容易翻车的地方

   💀 「我的检测器召回全是 1.00」
      → 你八成在检测器里 import 了污染器的常量。分开两个文件重写。

   💀 「误报率没测」
      → 只在被污染的数据上评估,等于只算召回不算精确。
        ⭐ 必须留一份【完全没污染】的对照集,跑一遍看误报。

   💀 「用 describe() 当检测器」
      → 均值/分位数抓得到数值类的脏,抓不到编码错乱、类别漂移、跨列不一致。
        ⭐ 十种形态需要至少四类不同的检查手段。

🥈 项目二:建一套质量门禁接进 CI(1–2 天)

目标:把第 8 章那六个维度写成断言,跑在一份真实数据集上,并且故意让它红一次

六个维度一个都不能少:完整性 / 唯一性 / 有效性 / 一致性 / 时效性 / 分布稳定性

import sys, numpy as np

CHECKS = []                                    # ⭐ 断言注册表:每条都带维度和等级
def check(dim, level="hard"):
    def deco(fn): CHECKS.append((dim, level, fn.__name__, fn)); return fn
    return deco

@check("完整性")
def 关键列非空(d):
    bad = int(np.sum([x is None or x == "" for x in d["city"]]))
    return bad / len(d["city"]) <= 0.001, f"空值率 {bad/len(d['city']):.4f}"

@check("唯一性")
def 主键唯一(d):
    return len(set(d["user_id"].tolist())) == len(d["user_id"]), "主键重复"

@check("分布稳定性", level="warn")             # ⭐ 分布类默认只 warn,别一上来就 hard
def 金额PSI(d, base_q=None):
    q = np.percentile(d["amount"], [10, 50, 90])
    return True, f"分位数 {q.round(1).tolist()}"

def run_gate(d):
    hard_fail = 0
    for dim, level, name, fn in CHECKS:
        ok, msg = fn(d)
        if not ok:
            print(f"[{'FAIL' if level=='hard' else 'WARN'}] {dim}/{name}: {msg}")
            hard_fail += (level == "hard")      # ⭐⭐ 只有 hard 才决定退出码
    sys.exit(1 if hard_fail else 0)             # ⭐ CI 靠这个退出码变红

✅ 通关判据(带数字)

判据 参考值
断言总数 ≥ 18 条,六个维度每维至少 2 条
hard 占比 ⭐⭐ ≤ 1/3。其余是 warn —— 全 hard 的门禁两周内一定会被加 --skip
运行时间 100 万行 < 60 秒(超了就要抽样,但抽样要在报告里写明)
故意注入一次故障,CI 必须红 且报告要指出哪一维、哪一列、多少行、样例值
误报 过去 14 天的真实数据回放,hard fail ≤ 1 次
阈值来源 每个阈值都要能说出是从哪段历史数据的哪个分位数来的

「hard 占比 ≤ 1/3」这条是整个项目的重点。 一个把所有断言都设成 hard 的门禁,看起来最严格 —— 然后第一次误报之后,就会有人在 CI 里加一个跳过参数,从此再没人删掉它。 ⭐⭐ 门禁的目标不是"拦住一切",是"红灯出现时所有人都相信它"。

⚠️ 最容易翻车的地方

   💀 「阈值是拍脑袋定的」
      → 必须用历史数据回测:把过去 14 天逐日跑一遍,
        看每条断言的取值分布,阈值放在【历史 p99 之外】而不是"感觉"上。

   💀 「门禁只跑在抽样上,线上还是炸了」
      → 唯一性、跨列一致性这两类【不能抽样】——
        抽样恰好会把重复和边缘不一致漏掉。 ⭐

   💀 「红了以后没人知道该干嘛」
      → 每条断言必须带三样东西:负责人、样例坏行、修复动作。
        只报"完整性检查失败"的门禁等于没有。

🥇 项目三:标注一致性实验(1–2 天)

目标:按第 9 章自己写一份标注指南,标 100 条两遍算 Kappa,改完指南再标一遍,看涨多少

import numpy as np

def kappa(M):
    """M 是混淆矩阵(两遍标注的交叉表)。⭐ p0 和 pe 必须一起报"""
    M = np.asarray(M, float); n = M.sum()
    p0 = np.trace(M) / n                                  # 观察一致率
    pe = (M.sum(0) * M.sum(1)).sum() / n ** 2             # ⭐ 瞎猜也能达到的一致率
    return round(p0, 3), round(pe, 3), round((p0 - pe) / (1 - pe), 3)

print(kappa([[26, 7, 1], [9, 20, 8], [1, 7, 21]]))    # 第一版指南 → (0.67, 0.335, 0.504)
print(kappa([[31, 3, 0], [4, 29, 4], [0, 3, 26]]))    # 修订后     → (0.86, 0.336, 0.789)
print(kappa([[92, 3], [3, 2]]))                       # ⭐⭐ 陷阱   → (0.94, 0.905, 0.368)

📏 参考数字(都是上面那段代码实跑出来的)

轮次 p₀(观察一致率) pₑ(碰运气一致率) κ
第一版指南 0.670 0.335 0.504
修订后 0.860 0.336 0.789
极不均衡的陷阱案例 💀 0.940 0.905 0.368

💀 看最后一行一致率 94%,Kappa 只有 0.368。 因为 95% 的样本都是同一个类别,两个人闭着眼睛全标多数类也能拿到 90.5% 的一致率。 ⭐⭐ 所以"我们标注一致率 94%"这句话,在类别不均衡时没有任何信息量 —— 报 κ 的时候必须把 p₀ 和 pₑ 一起报出来,只报 κ 也不够。

✅ 通关判据(带数字)

判据 参考值
第一轮 κ 0.45–0.60。> 0.85 说明任务太简单或你在背自己的答案
修订后 κ ≥ 0.75,且提升 ≥ 0.20
分歧样本占比 第一轮 25–35% → 第二轮 ≤ 15%
指南修订条数 ≥ 8 条,每条对应一个真实分歧,不许写"注意仔细"
留出集 ⭐⭐ 那 30 条没看过的,κ 提升至少要有前 70 条的一半

留出集那一条是本项目唯一的科学性保证。 盯着分歧样本改指南,κ 一定会涨 —— 那可能只是你把这 100 条的答案编码进了指南只有留出集上也涨,才说明你真的把规则说清楚了。

⚠️ 最容易翻车的地方

   💀 「两遍间隔两小时,κ = 0.93」
      → 你在背答案。⭐ 至少隔 48 小时,并且【打乱顺序 + 隐藏第一遍结果】。

   💀 「只报一致率不报 Kappa」
      → 见上面那个 0.94 / 0.368 的例子。

   💀 「指南越写越长,κ 反而掉了」
      → 常见且真实。⭐ 修订的正确方向是【把模糊的定义换成可判定的规则 + 边界样例】,
        不是【加更多描述性形容词】。规则要能让人回答"是/否",而不是"感觉像"。

🏆 挑战项目:复现一次数据泄漏(4–6 天)

难度 ★★★★ | 练第 14 章

   目标:做出一个【离线 AUC 0.99、去掉泄漏后掉到 0.72】的完整例子,
        并把"怎么发现的"写成一份复盘。

🧰 起步骨架(直接抄)

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score

rng, N = np.random.default_rng(11), 60_000
tenure  = rng.exponential(180, N)          # 在网天数
mau     = rng.poisson(12, N)               # 月活跃天数
tickets = rng.poisson(0.4, N)              # 工单数
plan    = rng.choice([0, 1, 2], N, p=[.6, .3, .1])
logit   = -1.4 - .005 * tenure - .115 * mau + .45 * tickets + .5 * plan
y = (rng.random(N) < 1 / (1 + np.exp(-logit))).astype(int)      # 是否流失,约 5.6%

# ⭐ 泄漏一:后果特征 —— 退款工单,只有【已经流失的人】才会开
refund = rng.poisson(np.where(y == 1, 1.4, 0.02))
# ⭐ 泄漏二:时间穿越 —— 账户状态最后更新距今天数,在流失【之后】被系统改写
gap    = np.where(y == 1, rng.gamma(2, 3, N), rng.gamma(2, 30, N))

base = np.c_[tenure, mau, tickets, plan]
tr   = np.arange(N) < 45_000
def auc(X, name):
    m = HistGradientBoostingClassifier(max_iter=150, random_state=0).fit(X[tr], y[tr])
    a = roc_auc_score(y[~tr], m.predict_proba(X[~tr])[:, 1])
    print(f"{name:16s} AUC={a:.4f}"); return a

auc(np.c_[base, refund, gap], "两个泄漏都在")   # 0.9899
auc(np.c_[base, gap],         "只有时间穿越")   # 0.9789
auc(np.c_[base, refund],      "只有后果特征")   # 0.9237
auc(base,                     "清洁版")        # 0.7168   ⭐⭐ 这才是真实能力

阶段一:造出泄漏(1 天)

阶段二:用四种手段把它找出来(2 天)

阶段三:修掉并复盘(1–2 天)

✅ 通关判据(带数字)

判据 参考值
泄漏版离线 AUC ≥ 0.98(骨架是 0.9899)
去泄漏后 ⭐⭐ 0.70–0.75(骨架是 0.7168)—— 这 0.27 就是这个项目的全部内容
单特征 AUC 扫描 泄漏特征单独就能到 0.90+;清洁特征最高的那个通常 < 0.68
时间切分 vs 随机切分 清洁版差 < 0.01;有"同人跨集"泄漏时差 > 0.05
剔除实验 删掉泄漏特征,AUC 掉 > 0.15;删掉任一清洁特征,掉 < 0.05
复盘文档 必须逐特征回答 T7 那句话,并且写明每个特征的生成时刻

⭐⭐ T4 的单特征 AUC 扫描是性价比最高的探针:一个单列就能到 0.90 的特征, 在真实业务里几乎不存在 —— 如果它出现了,先假设是泄漏,再去证明它不是。 而 T7 那句问话是唯一不依赖任何统计量的手段: 泄漏的本质是"用了预测时刻还不存在的信息",这是一个时间问题,不是一个数值问题。

⚠️ 最容易翻车的地方

   💀 「我只用随机切分,指标一直很漂亮」
      → 时间穿越型泄漏在随机切分下【也是高分】。
        ⭐ 时间切分是发现它的最低成本手段,也是这个项目的第一道检查。

   💀 「删了泄漏特征,AUC 还是 0.95」
      → 你漏了它的衍生特征(refund 的对数、gap 的分箱、
        以及任何用它算出来的交叉特征)。⭐ 按【血缘】删,不是按列名删。

   💀 「用特征重要性找泄漏」
      → 重要性高 ≠ 泄漏,重要性低 ≠ 干净。
        ⭐⭐ 重要性只能帮你【排查顺序】,判定必须回到"生成时刻"这一条。

   💀 「0.72 太难看了,我想把它调上去」
      → ⭐ 这正是这个项目要治的病。0.72 是真的,0.99 是假的。
        一个项目组愿不愿意接受这一点,比任何技术手段都重要。

📝 复盘模板

# 数据事故 / 实验:____
## 我注入了什么(真值台账)
## 第一个发现它的信号是什么,来自哪个检查
## 我用了哪几种手段,各花了多久
## 没能发现的部分 —— 为什么?需要什么信息才能发现?   ← 最有价值的一节
## 修复后的真实指标 vs 修复前的假指标
## 要加什么,才能让下次在【进模型之前】就被拦住

🔗 这一章连到哪里

去哪 为什么
模型上线之后 20 实战与挑战项目 ⭐⭐ 和这一章成对:那边的沙盘注入"上线之后发生的事"(漂移、管道坏、训练推理不一致),这边注入"数据本身的毛病"。两套连起来就是完整的一条链
Kaggle 23 数据泄露与外部数据 挑战项目的竞赛版:那边泄漏是可以"利用"的(榜单奖励它),这边是纯粹的事故
智能体 19 挑战项目B 评测驱动开发 ⭐ 「先建评测,再写实现」的同一条纪律 —— 项目一的"先写真值台账"就是它的数据版
模型上线之后 04 训练推理一致性 项目二的门禁往下游延伸一步:同样的断言要在推理侧再跑一遍

✅ 检查点

  1. 这四个项目共同的那条纪律是什么?为什么"真值台账"必须先写?
  2. 项目一里,为什么污染器和检测器必须分开写?共用常量会导致什么?
  3. 项目一的参考数字里,哪两种形态的召回最低?"单位不一致"为什么永远查不干净,该靠什么解决?
  4. 项目一为什么必须留一份没被污染的对照集?
  5. 项目二里"hard 占比 ≤ 1/3"这条为什么是重点?全 hard 的门禁会怎么死?
  6. 项目二里哪两类检查不能抽样?为什么?
  7. 项目三那个"陷阱案例"的 p₀、pₑ、κ 各是多少?它说明了什么?
  8. 项目三为什么必须留 30 条不看?不留会发生什么?
  9. 挑战项目里,泄漏版和清洁版的 AUC 各是多少?单特征 AUC 扫描的判读标准是什么?
  10. 为什么"用特征重要性找泄漏"是错的?唯一不依赖统计量的判定手段是什么?
👀 答案
  1. 你必须知道真值,否则没法给自己判卷。 真实工作里你永远不知道数据里到底有几处脏、泄漏藏在哪一列;自己造问题是唯一能拿到答案的办法。台账要先写,是因为事后补记的台账一定会漏,而漏掉的那几处正好会被算成"检测器没错"。
  2. 因为检测器一旦 import 了污染器的常量(比如那份哨兵字典),测的就不是检测能力而是复制粘贴能力 —— 召回会假性地变成 1.00。正确做法是两边分开文件,检测器那份字典自己从头列一遍,你会立刻发现自己漏了好几个。
  3. 跨列关联不一致(约 0.30)和近似重复(约 0.60),其次是单位不一致(0.71)。单位不一致查不干净是因为一笔真实的 8,800 元订单和一笔被乘了 100 的 88 元订单在数值上完全无法区分 —— 需要的不是更好的统计量,是契约里的单位字段(第 3 章)。⭐ "哪些脏靠看数据查不出来"这份清单比"我查出了 8 种"有用得多。
  4. 因为只在被污染的数据上评估等于只算召回不算精确。必须在完全没污染的对照集上跑一遍看误报率,通关线是被误标的行 < 1%
  5. 因为一个全 hard 的门禁看起来最严格,但第一次误报之后就会有人在 CI 里加跳过参数,从此再没人删掉它。⭐⭐ 门禁的目标不是"拦住一切",是"红灯出现时所有人都相信它" —— 所以还要求"过去 14 天真实数据回放,hard fail ≤ 1 次"。
  6. 唯一性和跨列一致性。因为抽样恰好会把重复和边缘不一致漏掉 —— 重复的两条可能一条被抽中一条没有,跨列冲突本来就是长尾。
  7. p₀ = 0.940,pₑ = 0.905,κ = 0.368。 说明类别极不均衡时,两个人闭着眼全标多数类也能拿到 90.5% 的一致率,所以 "我们一致率 94%"这句话没有任何信息量。⭐⭐ 报 κ 时必须把 p₀ 和 pₑ 一起报,只报 κ 也不够。
  8. 因为盯着分歧样本改指南,κ 一定会涨 —— 那可能只是把这 100 条的答案编码进了指南。留出 30 条不看,只有留出集上也涨(至少有前 70 条一半的提升),才说明规则真的说清楚了。这是本项目唯一的科学性保证。
  9. 两个泄漏都在 0.9899 / 只有时间穿越 0.9789 / 只有后果特征 0.9237 / 清洁版 0.7168 —— 那 0.27 就是整个项目的内容。单特征扫描判读:泄漏特征单独就能到 0.90+,而清洁特征里最高的通常 < 0.68;一个单列能到 0.90 的特征在真实业务里几乎不存在,先假设是泄漏,再去证明它不是
  10. 因为重要性高 ≠ 泄漏,重要性低 ≠ 干净 —— 重要性只能决定排查顺序。唯一不依赖统计量的判定是 T7 那句时点审计:「在做预测的那一刻,这个值真的已经存在了吗?」泄漏的本质是"用了预测时刻还不存在的信息",这是一个时间问题,不是一个数值问题。

🛑 可以停在这里

走神救援

这四个项目共同的那条纪律:你必须知道真值,否则没法给自己判卷。 真实工作里你永远不知道数据里有几处脏、泄漏藏在哪列,自己造问题是唯一能拿到答案的办法;而且台账必须先写,事后补记一定会漏,漏掉的那几处会被算成"检测器没错"。💀开工前的第一条规矩:污染器和检测器不能共用任何常量 —— 检测器一旦 import 了污染器那份哨兵字典,测的就不是检测能力而是复制粘贴能力,召回会假性地变成 1.00。🥉项目一:亲手弄脏一份干净数据再自己查出来。 注入第 4 章那十种形态,另开文件写检测器,召回和精确必须一起报。⭐参考召回:类型漂移 0.99、不可能值 1.00、空值伪装 1.00(字典全的话)、编码错乱 0.95、类别漂移 0.95、截断 0.90、时间格式 0.80、单位不一致 0.71、近似重复 0.60、跨列关联不一致 0.30 💀。通关线:十种里 ≥ 7 种召回 > 0.5,且在没被污染的对照集上误报 < 1%。 ⭐⭐这个项目最有价值的是 T5 而不是 T3"单位不一致"永远查不干净,因为一笔真实的 8,800 元订单和一笔被乘了 100 的 88 元订单在数值上完全无法区分 —— 需要的不是更好的统计量,是契约里的单位字段"哪些脏靠看数据查不出来"这份清单,比"我查出了 8 种"有用得多。 🥈项目二:把第 8 章六个维度写成断言接进 CI,并故意让它红一次。 六维一个都不能少:完整性/唯一性/有效性/一致性/时效性/分布稳定性;断言 ≥ 18 条、每维 ≥ 2 条、100 万行 < 60 秒。⭐⭐最重要的一条通关判据是「hard 占比 ≤ 1/3」:全 hard 的门禁看起来最严格,然后第一次误报之后就会有人在 CI 里加跳过参数,从此再没人删掉它 —— 门禁的目标不是"拦住一切",是"红灯出现时所有人都相信它";所以还要求过去 14 天真实数据回放 hard fail ≤ 1 次,每个阈值都要说得出是从哪段历史的哪个分位数来的。💀两类检查不能抽样:唯一性和跨列一致性 —— 抽样恰好会把重复和边缘不一致漏掉。红了以后每条断言必须带负责人、样例坏行、修复动作,只报"完整性检查失败"等于没有。🥇项目三:标注一致性实验。 自己写指南 → 标 100 条 → 隔至少 48 小时并打乱顺序再标一遍 → 算 Cohen's Kappa → 只看前 70 条的分歧修订指南(留 30 条当验证集)→ 重标。⭐实跑数字:第一版 p₀=0.670 / pₑ=0.335 / κ=0.504,修订后 p₀=0.860 / pₑ=0.336 / κ=0.789;💀陷阱案例 p₀=0.940 但 κ 只有 0.368 —— 因为 95% 的样本是同一类,两个人闭着眼全标多数类也能拿 90.5% 的一致率,⭐⭐所以"我们一致率 94%"这句话在类别不均衡时没有任何信息量,报 κ 必须把 p₀ 和 pₑ 一起报通关线:第一轮 κ 0.45–0.60(>0.85 说明你在背自己的答案)、修订后 ≥ 0.75 且提升 ≥ 0.20、分歧样本从 25–35% 降到 ≤ 15%、指南修订 ≥ 8 条且每条对应一个真实分歧。⭐留出集是本项目唯一的科学性保证:盯着分歧样本改指南 κ 一定会涨,那可能只是把这 100 条的答案编码进了指南,只有那 30 条没看过的也涨(至少有前 70 条一半的提升)才算数修订的正确方向是把模糊定义换成可判定的规则 + 边界样例,不是加更多形容词。 🏆挑战项目:复现一次数据泄漏(4–6 天)。 骨架实跑:两个泄漏都在 AUC 0.9899、只有时间穿越 0.9789、只有后果特征 0.9237、清洁版 0.7168 —— ⭐⭐那 0.27 就是整个项目的全部内容。两种泄漏分别是后果特征(退款工单,只有已流失的人才会开)和时间穿越(账户状态更新时间,在流失之后被系统改写)。四种发现手段:⭐T4 单特征 AUC 扫描(性价比最高的探针)—— 泄漏特征单列就能到 0.90+,清洁特征最高的通常 < 0.68;一个单列能到 0.90 的特征在真实业务里几乎不存在,先假设是泄漏再去证明它不是;T5 时间切分 vs 随机切分(清洁版差 < 0.01,同人跨集时差 > 0.05);T6 逐特征剔除(删泄漏特征掉 > 0.15,删清洁特征掉 < 0.05);⭐⭐T7 时点审计:对每个特征问一句「在做预测的那一刻,这个值真的已经存在了吗?」 —— 这是唯一不依赖任何统计量的手段,因为泄漏的本质是"用了预测时刻还不存在的信息",这是一个时间问题不是一个数值问题。💀四个翻车点:①只用随机切分 → 时间穿越型泄漏在随机切分下也是高分,时间切分是第一道检查 ②删了泄漏特征 AUC 还是 0.95 → 漏了它的衍生特征,要按血缘删不是按列名删 ③用特征重要性找泄漏 → 重要性高≠泄漏、低≠干净,它只能决定排查顺序 ④「0.72 太难看想调上去」→ ⭐这正是这个项目要治的病:0.72 是真的,0.99 是假的,一个项目组愿不愿意接受这一点,比任何技术手段都重要。

下一节 👉 附录A-速查.md

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