98.6% 的准确率是坏消息:三种泄漏
这一章讲的是这门课里最贵的一个错误,因为它不报错、不警告、还让你的指标变好看。发现它的唯一时机是在做之前就知道它长什么样。三种泄漏我会各跑一遍给你看,其中一个会把准确率顶到 98.6%,另一个只值 1.9 个百分点——而后者才是更危险的那个。
先看三个数字
下面这台在同一份数据上跑三种做法:A 把泄漏列留在特征里,B 先在全量数据上填补和标准化再划分,C 规规矩矩地先划分、只用训练集的统计量。三个标签页切一遍:
| 做法 | 准确率 | F1 | AUC | 误报 FP |
|---|---|---|---|---|
A · 目标泄漏(留着 rate_reported) | 0.986 | 0.983 | 0.999 | 0 |
| B · 预处理泄漏(全量填补后划分) | 0.875 | 0.852 | 0.943 | 9 |
| C · 干净做法 | 0.856 | 0.835 | 0.920 | 13 |
只有 C 那一行的数字,是你把模型交给卫生部之后真正能拿到的。A 和 B 都是自己骗自己,区别只在骗得多明显。
泄漏一:目标泄漏——特征里躺着答案
示例数据里有一列 rate_reported:卫生局上报表里的死亡率,整数。它和目标 u5mr 的相关系数是 0.9998——因为它们本来就是同一个事实的两种记录,只差一次四舍五入。
把它留在特征里,决策树的第一刀就是:
上报死亡率 <= 25.5 (n=336, gain=0.47)
目标阈值是 25,它劈在 25.5。模型没有学到任何东西,它只是把答案抄了一遍。准确率 98.6%、AUC 0.999、零误报——这些数字在报告里看起来是最漂亮的一页,实际上是最致命的一页,因为上线时 rate_reported 根本不存在(你要预测的正是它)。
怎么认:① 单列就能把 AUC 顶到 0.98 以上;② 那一列的名字里有「结果」「上报」「登记」「实际」「最终」这类词;③ 问自己「预测的那一刻,这个值真的已经知道了吗?」——第三条是最可靠的判据。
自检:算每个候选特征与目标的相关系数,从高到低排,看前三名。
corr = df[features + [target]].corr()[target].drop(target).abs().sort_values(ascending=False) print(corr.head()) # 任何 > 0.95 的,先假设是泄漏,去查它的定义再决定
示例数据里这样一查,rate_reported(0.9998)和 deaths_registered(0.953)会立刻排在最前面。这个检查属于 Step 2.3,成本五分钟;等到 Step 8 才发现,代价是重跑整条链、重写三个小节。
更隐蔽的一类是「未来信息」泄漏:比如用「该地区次年的卫生预算」预测今年是否达标——预算是根据今年的结果定的。这类列的相关系数可能只有 0.6,光看数字看不出来,只能靠第 ③ 条那个问题。
泄漏二:预处理泄漏——只值 1.9 个百分点,但更危险
做法 B 唯一的错误是顺序:先在全部 480 行上算中位数并标准化,然后才划分训练/测试。于是测试集的信息(它参与了中位数的计算)漏进了训练过程。
结果:准确率 0.875 vs 干净做法的 0.856,虚高 1.9 个百分点。
这个数字很小,而我要诚实地把它报出来——很多教程会说预处理泄漏「后果严重」,在这份 480 行、信号很强的数据上,它并没有。但这恰恰是它更危险的原因:
- 幅度不可预测。在小样本(几十行)、高维(列数接近行数)、或者你用全量数据做过特征选择的场合,同一个错误能把 0.6 的 AUC 吹成 0.9。你无法从虚高后的数字推断虚高了多少。
- 方向永远一致:只会高估,不会低估。所以它不会被你的怀疑抓住——你看到的总是「效果不错」。
- 它会污染你的所有比较。如果你用泄漏的流程比较六种填补方法(第 9 章那张表),排名可能是错的,而你据此做的选择也就错了。
| 操作 | 划分前? | 说明 |
|---|---|---|
| 去重、修量纲错、类别规范化、日期格式统一 | 可以 | 这些是「修正记录本身」,不涉及跨行统计量 |
| 缺失填补(均值/中位数/KNN) | 不行 | 统计量必须来自训练集 |
| 标准化/归一化 | 不行 | 同上 |
| PCA、特征选择 | 不行 | 它们都看了目标或全体分布 |
| 目标编码 | 不行(而且要折内做) | 最容易泄漏的编码方式 |
| 过采样/SMOTE | 不行 | 只对训练集做。对测试集重采样是很常见的严重错误——测试集必须保持真实分布 |
一条能覆盖全部情况的规则:任何需要「看多行数据才能算出来的数字」,都必须只从训练集算。
泄漏三与泄漏四:时间与分组
时间泄漏
如果你的数据有时间维度(这门课的 SDG 面板数据几乎必然有),随机划分就是用未来预测过去。模型见过 2022 年的数据,然后你让它「预测」2019 年——这在验证上毫无意义,因为真实部署时你只能用过去预测未来。
规则:只要用了滞后/滑窗特征,或者你的部署场景是「预测下一期」,划分必须按时间切。本书示例的两种划分方式在第 17 章和第 21 章都会出现:随机分层划分用于「同期不同地区」的推广(横截面),按时间划分用于「预测未来年份」(第 21 章那张模型退化表就是这么做的)。两种都合法,但你必须说明你的部署场景对应哪一种。
分组泄漏
同一个实体的多条记录被划到了两边。示例数据里「北区 2019」如果重复了两条(第 10 章去掉的那 7 条重复行),一条在训练集、一条在测试集,测试分数就会虚高。
更常见的形态是面板数据本身:北区 2015–2023 有 9 行,随机划分后其中 6 行在训练集、3 行在测试集。严格来说这也是分组泄漏(模型见过这个地区的其他年份),要不要处理取决于你的问题:
- 如果决策是「在已知的这些地区里排序」→ 随机划分合理,因为部署时这些地区也都在。
- 如果决策是「把模型用到没见过的新地区」→ 必须按地区分组划分(
GroupKFold),否则会严重高估。
这个判断本身就是 Step 7.1「create and justify test designs」要的东西——不是选一个划分方式,是论证为什么这个划分方式对应你的部署场景。
那句能拿分的话
把这一章的全部内容压成两句写进报告,一句在 3.1,一句在 7.1:
3.1 Select the data(末尾) 下列变量虽与目标高度相关,但经核对属目标变量的同源记录, 若纳入特征集将构成目标泄漏(target leakage),故予排除: · rate_reported(上报死亡率,与 u5mr 相关系数 0.9998) · deaths_registered(登记死亡数,相关系数 0.953) 排除依据:在预测时点(次年预算决策前),这两个变量尚不可得。 7.1 Test design(开头) 数据按目标变量分层随机划分为训练集 70%(336 行)与测试集 30%(144 行),随机种子固定为 42 以保证可复现。所有填补、 缩放与降维参数仅由训练集估计,并以相同参数变换测试集 (以 sklearn Pipeline 实现),以避免预处理泄漏。业务键重复 已在 3.2 去除,避免同一记录同时出现于两侧。 本项目的部署场景为在既有地区间排序,故采用随机分层划分; 若需外推至未观测地区,应改用按地区分组的交叉验证 (GroupKFold),此局限在 8.4 中说明。
这两段话加起来不到 200 字,是全书性价比最高的 200 字。它同时覆盖了 Step 3.1、7.1 的评分点,还给 8.4 埋了一个局限性讨论。
- □ 算过每个特征与目标的相关系数,最高的那几个都能解释清楚为什么高
- □ 每个特征都通过了「预测那一刻真的已知吗」这个问题
- □ 填补、缩放、PCA、特征选择全部在划分之后,且只 fit 训练集
- □ 重采样只对训练集做,测试集保持原始分布
- □ 业务键去重在划分之前完成
- □ 有滞后/滑窗特征 → 按时间划分;要外推到新实体 → 按实体分组划分
- □ 指标好得反常时(准确率 > 0.97、AUC > 0.99),先怀疑泄漏,而不是先高兴
最后一条是这一章唯一需要记住的态度。在这门课里,一个 0.856 的准确率配一段清楚的测试设计说明,比一个 0.986 的准确率得分高——因为后者会被追问,而你答不上来。
1. Step 3.1 末尾的泄漏排除段(上面模板),含被排除列的相关系数与排除依据。
2. Step 7.1 开头的测试设计段(上面模板),含划分比例、种子、Pipeline 声明、去重声明、划分方式的论证。
3. 可选但很值的一张表:如果你时间够,把「留着泄漏列」和「排除后」两组指标都跑出来做对照,配一句「前者准确率 0.986 但不可部署」。主动展示一个你避开的坑,是 Critical Thinking 那条毕业生能力最直接的证据。
这一章的一句话
目标泄漏会把准确率顶到 98.6%、AUC 顶到 0.999、误报清零,而模型什么都没学到;预处理泄漏在这份数据上只值 1.9 个百分点,但它的幅度不可预测、方向永远是高估——所以唯一的防线是顺序纪律:任何需要看多行才能算出的数字,都只能从训练集算。
卷 III 结束。数据准备完了,下一卷终于开始建模:Step 5 选方法、Step 6 选算法、Step 7 跑起来,合计 40% 的分。第 14 章先解决一个前置问题——你的目标该配八类任务里的哪一类。