🏠 总目录📚 本教程 附录A · 速查
📑 本页目录(点开跳转)

附录A · 速查

随时查,不用通读 | ⭐ 出问题时先看第一节


🎯 一句话

数据出问题不会报错 —— 它让你的任务成功地算出一个错答案,然后你满怀信心地把它上线了。


🚨 数据出问题了,从这里开始

   ① 变好还是变差   ⭐⭐ 变好也要查(分母被换掉会让一切变好看)
   ② 数据还是模型   离线线上差 > 0.1 时,规定【前 3 天不许碰模型】
   ③ 换种切法呢     随机切 vs 按时间切,差 > 0.03 就是泄漏      ⭐
   ④ 拆开看呢       按渠道/城市/新老/覆盖情况分组 —— 整体指标会稀释局部灾难
   ⑤ 这是今天的吗   38 条断言全绿也可能是昨天的拷贝
   ⑥ 谁不在数据里   被旧系统筛掉的那批人
症状 几乎总是 去哪
AUC >0.95,而这个问题本来很难 ⭐⭐ 代理变量或时间穿越 14
去掉一个特征 AUC 从 0.97 掉到 0.70 💀 代理变量,可以确诊 14
随机 CV 很稳,按时间切掉一大截 时间穿越 14
CV 折间标准差小得离谱(0.004) 💀 ⭐ 泄漏的指纹,不是稳定性 14
复杂模型比简单模型好得不成比例 重复样本跨集(只有它们记得住) 06
对老用户还行、对新用户明显差 分组泄漏(模型在"认人") 14
线上比离线差,但线上服务查下来是对的 point-in-time 违规 —— 离线错了 18
整体正常,某个渠道彻底坏掉 ⭐⭐ 枚举漂移/数据源只覆盖一部分人群 0305
缺失率监控全绿,数据却明显不对 💀 填充发生在监控之前 05
断言全过,行数波动 0.00% 💀 数据停更/复制了昨天的分区 08
按天看一切正常 ⭐ 聚合粒度把证据抹掉了(时区) 04
缓慢下降被判成"漂移" ⚠️ 灰度会把故障伪装成漂移 04
CTR/正样本率突然变好看 ⭐⭐ 分母被换掉了(上报口径变更) 02
离线 F1 很好,线上召回只有一半 标注口径和线上口径不是同一版 0109
同意率 90%,一致性却很差 类别不平衡,Kappa 可能只有 0.20 10
标签里有一批明显标错的 用置信学习一类方法定位可疑标签 11
下一批该标什么说不清 主动学习(⚠️ 冷启动、采样偏差两个陷阱) 12
换个评测集,选型结论就翻转 评测集的抽样/样本量/污染有问题 13
推荐排序越来越像同一种人 选择偏差 + 反馈回路,会自我加强 15
三个月前的模型重现不出来 上游被覆盖,或被静默 backfill 17
AUC 只差 0.002,个体分数却翻转 聚合指标掩盖个体决策的不稳定 17
有人说「我们已经匿名了」 问:最小等价类是多少人 19

判断脏数据危不危险只看一件事:它会不会让程序停下来。 会停的代价是几十分钟;不会停的代价是几周 × 每天的业务量。


🧹 脏数据十种形态速查

会报错的只有 ①②,其余八种全部静默通过 —— 90% 的排查精力花在会叫的,90% 的损失来自不叫的。

# 症状 怎么发现 怎么修
① 类型不一致 join 后行数莫名变少 「可转数值比例」在 0% 和 100% 之间最可疑 入口统一 dtype;⚠️ 别用 CAST(它把 "abc" 静默变 NULL
② 字符编码 中文乱码;列名多一个不可见字符 /BOM/零宽空格占比 全链路 UTF-8,读文件用 utf-8-sig
③ 时区 💀 数据量在某个整点"搬家" ⭐⭐ 按小时画直方图 + 循环互相关(按天完全不可见) 存 UTC epoch 毫秒;契约写 epoch_unittz
④ 单位 均值突然 ×100,分布形状不变 监控分位数不是均值;断言取值上限 ⭐ 字段名带单位 amount_centsdwell_ms
⑤ 字段截断 💀 ⭐⭐ 一批不同实体挤成同一个值 长度直方图在 64/255 这类边界上出现尖峰 ⚠️ 不可逆,只能回上游重取;长 ID 一律当字符串
⑥ 默认值伪装成真值 💀 年龄 0 占 8%、首单 1970-01-01 占 3% ⭐⭐ 算每列「最高频单值占比」(连续变量不该有值占到 1%) 入口显式转 NULL + 保留缺失指示列
⑦ 枚举漂移 映射表 default 分支占比缓慢上涨 契约列枚举全集;⭐ 监控「未知值占比」这条曲线本身 禁止 dict.get(x, default) 静默兜底
⑧ 空格与全半角 groupby 出现两个"一模一样"的分组 归一化后类别数掉 >1% 就是带病工作 unicodedata.normalize("NFKC", s).strip() 一行解决八成
⑨ 嵌套 JSON 变结构 解析出的列突然全 NULL,而 JSON 完全合法 采样收集「路径 + 叶子类型」逐天对比 上游拍平成物理列;解析失败要计数
⑩ 同名不同义 💀 ⚠️ 匹配率 60% 这种不高不低最致命 join 前算交集率,低于 95% 必须问人 靠契约的语义定义;加系统前缀 crm_user_id

⭐⭐ ⑤ 和 ⑩ 被低估最多,因为它们不产生任何异常值 —— 取值域校验、缺失率监控、异常检测对它们全盲30 分钟体检:扫 P0/P1 → ⭐ 每个字符串列打印长度直方图 + Top-20 取值(十种里有六种靠人扫一眼发现 —— 规则只能查你想到的,Top-20 能让你看见你没想到的)→ 数值列 p1/p50/p99 → 时间列按小时互相关 → 每个 join 先算交集率。


⬜ 缺失三机制

df.fillna(df.mean()) 是这套教程里最贵的一行代码。

缺失取决于 能从数据里验证吗 正确处理后能无偏吗
MCAR 什么都不取决于 ✅ 可以证伪
MAR 已观测的其他字段 ⚠️ 不能证明,只能证伪 MCAR
MNAR 缺失的值本身/未观测的东西 原则上不可能 💀 ❌ 任何填充都有偏

⭐⭐ MAR 里的 R 骗了所有人:不是「随机」,是「在你已经看到的东西的条件下随机」。 💀 MAR 和 MNAR 无法用数据区分 —— 判断机制永远是业务判断,数据只能帮你排除 MCAR。

实跑(真值 b1 = 1.500b2 = −0.800,缺失约 33%):

处理方式 MAR 下 b1 偏差 MNAR 下 b1 偏差
上帝视角 +1.4994 −0.0%
均值填充 💀 +1.2417 −17.2% +1.0560 −29.6%
均值填充 + 缺失指示 +1.3318 −11.2% +1.2077 −19.5%
整行删除/回归填充 +1.4970 −0.2% +1.3601 −9.3%
随机回归填充(只用 x2) ⚠️ +0.9981 −33.5%
随机回归填充(x2 + y) +1.4900 −0.7%
   ① 偏差会传染给你根本没动过的特征  ⭐⭐
      均值填充只碰了 x1,却把 x2 的系数从 −0.80 打到 −0.27(错 66%)
      —— 填充改变的是【相关结构】,不只是那一列
   ② 看起来最"讲究"的方法偏得最厉害
      随机回归填充恢复了方差(1.004 vs 原始 1.003),b1 却偏 −33.5%
      原因只有一个:填充模型里没有 y。加上 y 立刻回到 −0.7%
      ⚠️ 但预测建模不能用这招 —— 推理时你没有 y
   ③ 同一个方法 MAR 下无偏、MNAR 下有偏(−0.2% vs −9.3%)
      而你无法从数据里分辨自己处在哪一种                     💀

均值填充五条罪状:压缩方差(std 1.003→0.781)/稀释相关(corr 0.599→0.376)/造假尖峰/抹掉「缺失」这条信息/训练推理不一致高发地。⭐ 它唯一的优点是不会报错。

缺失本身是信息(信贷违约实跑,缺失组违约率 23.98% vs 非缺失 13.71%,1.75 倍):均值填充 0.6540+缺失指示 0.6825(+0.0285,成本一列 0/1) / 填 -999 + 逻辑回归 0.6292(比什么都不做还差)。 ⚠️ 若字段是「标签发生后才被填上」的,_is_missing 就是标签副本 = 泄漏。

方法 什么时候用 什么时候会坏
原生支持缺失的模型 ⭐⭐ LightGBM/XGBoost,预测任务首选 需要可解释系数时
缺失指示 + 任意填充 几乎永远该加 缺失「事后才知道」时会泄漏
整行删除 MCAR/MAR 且缺失率 <5% MNAR 下有偏;缺失率高时样本大量流失
多重填充(MICE) 要统计推断、要置信区间 假设 MAR;预测场景不适用
均值/中位数填充 缺失率 <1% 且确认无所谓 其余所有情况 💀
常数哨兵值(−999) 只对树模型 线性模型/NN 上 0.654 → 0.629

五条铁律:统计量只从训练折算并进 pipeline / 训练推理同一份代码同一份统计量 / ⭐⭐ 缺失率必须在填充之前采集 / 缺失率漂移是最早的故障信号 / 缺失率 >1% 自动生成 _is_missing。 ⭐ MNAR 只能做三件事:变成 MAR(去采集解释缺失的那个变量)/ delta 敏感性分析(实跑 delta 从 −1.0 到 +0.5,b1 从 0.381 到 1.575)/ 显式建模。⭐ 它的价值不是找到「对的 delta」,而是能说出「只要低不超过 0.25 个标准差,结论就不变」


👯 重复与实体解析

   ① 精确重复     每个字节都一样        哈希,一行代码            ⭐ 有客观标准
   ② 近重复       同一内容的不同版本    MinHash/SimHash/pHash    ⚠️ 要定阈值
   ③ 同一实体多条  阻塞→相似→判定→合并                           💀 没有客观标准
      「两个分支机构算不算一个实体」——【做风控算一个,做配送算两个】
      ⭐ 先定义,再解析。反了会做出一个谁都不满意的结果

⚠️ 字段截断制造假重复,空格/全半角掩盖真重复 → 归一化必须在去重之前。

核心实跑(重复率 28.6%):随机切分后验证集 41.5% 的行在训练集里有孪生副本

模型 带重复 + 随机切分 按实体切分 虚高
1-NN 0.7834 0.6210 +0.1624 💀
随机森林(叶子=1) 0.9571 0.8349 +0.1222 💀
逻辑回归 0.7601 0.7260 +0.0341

⭐⭐ 最干净的证据:按「有没有孪生」把验证集拆开 —— 有孪生的部分 AUC = 1.0000,没孪生的 0.8788。前者不是预测,是检索。 ⭐ 虚高幅度和模型记忆力成正比;去重后指标会掉,掉的那部分本来就不属于你精确去重必查三个对象:全字段/业务主键/⭐ 去掉时间戳之后ts 差 1 秒哈希就不同)。⚠️ 浮点先 roundNaN != NaN 要先填占位串。

近重复没有好阈值(7 条商品标题,精确 MD5 找到 0 对):

阈值 命中
0.4 10 7 3 0
0.5 6 3 3 4
0.6 – 0.8 3 3 0 4
0.9 1 1 0 6

💀 更刺眼的是排序:不同型号的两款手机相似度 0.594,同一个商品只有 0.492 —— ⭐⭐ 字面上更像的业务上可能不同。阈值必须用人工标注的样本对来定,按「漏一对 vs 错合一对」谁代价大来偏。 阻塞(20 万条):全量 2.000e+10 对 → 按城市 5.883e+07(1/339)→ 城市+姓氏首字母 2.262e+061/8842)。⚠️ 只降召回不降精确 → ⭐ 多个阻塞键取并集,丢弃超大桶。

⚠️ 传递闭包的相变

误判率 误连对数 簇数(真值 5000) 最大簇 = 几个真实体
0 0 5000 3 1
1e-4 300 4700 15 5
5e-4 1500 3500 69 23
1e-3 3000 2015 4413 1471 💀
2e-3 6000 526 13104 4368
5e-3 15000 11 14970 4990

💀 误判率只翻一倍,最大簇从 23 个实体炸到 1471 个(64 倍) —— 随机图巨型连通分量的相变,不是线性恶化。 ⭐⭐ 三件事:簇大小设硬上限(超限整簇打回人工)/监控「最大簇大小」曲线(相变最早的信号)/高风险场景禁用传递闭包四步顺序:归一化 → 去重 → 切分 → 权重。⭐⭐ 去重必须先于切分,反了等于没做。 ⚠️ 唯一不该去重的场景:重复反映真实业务事件 —— 用权重,不要用删除


🔬 质量门禁清单

维度 典型断言 能自动查吗
完整性 关键字段非空率 ≥99.5%;分区行数在区间内(值缺不缺 + 行缺不缺
唯一性 主键无重复
有效性 类型/范围/枚举白名单/正则/外键
一致性 pay_time ≥ create_time;明细 SUM = 汇总表
时效性 now − max(event_time) < 3h ✅(最常被漏掉)
准确性 抽样人工复核/和权威源对账 不能

准确性是唯一一个数据不能自己证明自己的维度 —— 它是抽样问题,不是全量扫描问题。 ⚠️ 整体比率最会骗人:「city 缺失率 0.4%」可能是某个新渠道 100% 缺失 —— 任何比率型断言都要有分组版本

级别 判据 例子 触发后
🔴 BLOCK 数据自己 100% 能证明这是错的 主键重复;类型变了;行数只有平时 3%;数据年龄 >24h 停发布,回退到上一个已知良好版本
🟡 WARN 只是「和昨天不一样」 缺失率 0.6%→2%;新枚举值;某渠道量翻倍 照常发布,报告高亮
INFO 只想留个趋势 分位数、类别占比、行数 只落库,画趋势图

💀 三个反模式:全设 BLOCK(第一次大促就会长出"强制通过"按钮,然后所有检查一起死)/BLOCK 了却没有回退目标/断言写在加工代码里。 ⭐ 门禁健康度的唯一诚实指标:「强制通过」被点击的次数 —— 每周超 3 次说明是断言定错了,不是人不守规矩。

阈值怎么定(90 天模拟:日缺失率 0.61%±0.29%,第 60 天真坏了跳到 3.96%):

阈值策略 报警次数 真事件前误报 真事件后命中
静态 > 1%(拍脑袋的整数) 32 2 次 30 / 30
中位数 + 3.5×MAD(28 天滚动窗) 15 1 次 14 / 30 💀
静态 > 2.80%(历史 P99 × 1.5) 30 0 次 30 / 30

阈值该从历史分布算,不该是好看的整数1% 落在日常波动的尾巴而不是尾巴)。 💀 纯动态阈值会把「持续变坏」学成新常态:MAD 线连报 14 天后彻底静默 —— 它只报「变化」不报「坏」→ 必须动态线 + 静态硬底线两条一起用。 再加三招:连续 2 批都超阈值才升级/分层阈值/⭐ 告警自带 sample_keys不带样例主键的失败断言等于没写)。 落地一周:只管进模型的前 20 个字段 → 每字段 3 条断言全设 WARN跑满 7 天收集真实分布再定阈值 → 第 2 周只把确定性错误升级为 BLOCK。⭐ 断言集由事故驱动生长,不由想象力驱动。 ⭐⭐ 一条便宜到离谱的断言md5(本批内容) != md5(上一批内容) —— 专治「上游悄悄停更/重复写入」,这类故障在每个传统维度上都表现完美


✍️ 标注

指南六个必需块

关键点
① 任务一句话 + 下游用途 标注员知道用途就能自己推理边界
② 每个标签的定义 正面定义 + 排除项 —— 好定义都写了「不是什么」
③ 正例/反例各 ≥3 从真实数据里挑,不许编(编的例子永远太典型)
边界样例库 ⭐⭐ 这才是指南的正文
⑤ 优先级/仲裁规则 没有它,每个人会自建一套
⑥ 不确定怎么办 ⚠️ 必须有合法出口("存疑" + 写一行理由)

⭐ 只说「是什么」的定义在模糊处提供不了信息 —— 标注员需要思考的样本几乎全是模糊样本;⚠️ 没有第 ⑥ 块,不确定就变成随机猜或者把「其他」变成垃圾桶。 边界样例四字段:原文/正确标签/⭐⭐ 为什么/对照反例 ——「为什么」让标注员能外推到没见过的样本。 ⭐ 按「类别对」组织,不按「类别」(分歧永远发生在两个类之间);边界样例字数应是定义字数的 3 倍以上。💀 指南太长的解法是换载体:定义 1 页封顶,样例做成工具里自动弹出的可检索表格。

试标注收敛

轮次 Fleiss' Kappa 相比上轮 新增争议
R1 0.41 37
R2 0.63 +0.22 12
R3 0.71 +0.08 3
R4 0.74 +0.03 1 ✅ 收敛

⚠️ R3 最容易被误判为收敛 —— 争议只剩 3 条,但 Kappa 还在以每轮 0.08 上涨;还在涨就说明指南还在变清楚判据两条必须同时满足:提升 <0.05 新增争议 <3 条/轮。⚠️ 每轮必须换新样本。⭐ 分歧会上只准问:「指南里的哪一句话,让你们做出了不同的判断?该改指南还是怪人:分歧集中在少数几对类别 → 指南;多数人朝同一方向错 → 指南;⭐⭐ 换一批全新标注员重标、分歧模式一样 → 100% 是指南问题(最硬的判据)。七到八成的低一致性来自指南歧义。

质检抽样量

三种手段不可互替:黄金集(3~5%,⚠️必须伪装)查态度/抽样复核给错误率分布/双标重叠才能测指南歧义。用公式不要拍比例 —— n = ln(1−C) / ln(1−p)

想发现的缺陷率 90% 置信 95% 置信 99% 置信
10% 22 29 44
5% 45 59 90
2% 114 149 228
1% 230 299 459

反过来 —— 抽 N 条全对能说多狠的话:50 条 ≤5.82%/100 条 ≤2.95%/200 条 ≤1.49%/500 条 ≤0.60%。 ⭐ 「抽 5%」这种比例式抽样是错的,该定绝对条数,和批次大小几乎无关。💀 抽 200 条全对不是「错误率 0%」—— 12 万条上就是 1,788 条错标

Kappa 四指标选型

指标 人数 允许缺失 标签类型 什么时候用
Cohen's Kappa 恰好 2 人 名义 双标重叠最常见的场景
Fleiss' Kappa 多人(每条固定 k 个) 名义 ⭐ 众包:不要求是同一批人
Krippendorff's α 任意 每条评分数可不同 名义/有序/区间 ⭐⭐ 最通用(真实平台数据总不规整)
加权 Kappa 2 人 有序 1~5 分、P0~P3、风险等级,必须用

⚠️ 多人场景两两算 Cohen 再平均 ≠ Fleiss,人数不齐时会系统性偏高。

⚠️ 为什么不能用同意率

某工单两人独立判「是不是投诉」,1000 条,混淆矩阵 [[883, 50], [50, 17]]

   Po = (883 + 17) / 1000 = 0.900              ← 90%!验收报告写得很好看
   Pe = 0.933×0.933 + 0.067×0.067 = 0.874978   ⭐ 闭眼乱按都能有 87.5%
   Kappa = (0.900 − 0.874978) / (1 − 0.874978) = 0.2001    💀

   那 90% 里 87.5 个百分点是白送的(来自"两人都说不是投诉")
   靠水平挣来的只有 2.5 个,最多能挣 12.5 个 —— 2.5/12.5 = 0.20

两人各标了 67 条投诉,共同认定的只有 17 条正类专属一致率 0.2537 vs 负类 0.9464。 ⭐ 同意率问「你们多常一致」,Kappa 问「你们比按同样比例乱按好了多少」—— 类别越不平衡,同意率越没意义。

用途 Kappa 下限 为什么
训练集 ≥ 0.60 低于此,标签噪声开始吃掉模型收益(第 11 章
评测集 ≥ 0.80 ⭐⭐ 评测集的一致性就是你能测出的性能上限第 13 章
高风险(医疗/风控/内容安全) ≥0.80 且必须有仲裁 错一条的代价不对称
主观任务 0.5 可能就是天花板 改用加权 Kappa 或偏好对比

⭐⭐ 人类之间的一致性是模型性能的天花板 —— 先看评测集的一致性,再看模型分数;顺序反了,后面全是幻觉。

Kappa 的坑 表现 该怎么办
Kappa 悖论 同样 Po=0.850、同样 150 条分歧:均衡时 0.700,不均衡时 0.128 ⭐ 三个一起报:Kappa + 正类专属一致率 + 分歧绝对条数
Bias effect 两人边际分布差很多时 Kappa 反而虚高 同时报两人各自的类别分布
测「一致」不测「对」 💀 两人一致地把广告标成普通评价 → Kappa = 1.00 一致性(双标重叠)和正确性(黄金集)是两套质检

有序标签必须加权(同一份 400 条 1~5 分):同意率 0.5175/无权 0.3964/线性 0.6747/二次 0.8510。⚠️ 只写「Kappa=0.85」没有信息量,必须写清权重。 定位靠分歧矩阵不靠总分(600 条 4 类,Kappa 0.6477,139 条分歧):投诉↔退款 88 条(63.3%)/咨询↔其他 51 条/其余 4 对 0 条;合并后 Kappa 变 0.8624。 ⭐ 中等的总分几乎总是少数几对类别拖的 —— 修那两对比培训所有人有用一百倍。 ⚠️ 但合并只是诊断不是解药留着一对标注员分不开的类别,模型也一定分不开。下一批标什么是主动学习的问题,⚠️ 它有冷启动采样偏差两个陷阱 —— 第 12 章。⭐ 评测集要单独看五件事:抽样、样本量与置信区间、污染、评分器校准、结果怎么报 —— 第 13 章


💧 泄漏七种来源

   三个问题,问每一个特征、每一条流程:
   ① 时间:这个值在【预测那一刻】已经产生并且查得到吗?
   ② 身份:它的计算用到过【这一行的标签】吗?—— 包括间接(编码/填充/采样/筛选)⭐
   ③ 边界:训练集和测试集之间有没有【同一个东西】跨过去?
# 泄漏 有→无 症状 怎么验证 怎么修
时间穿越 0.810 → 0.682 随机 CV 稳、按时间切掉一截 两种切法都跑,差 >0.03 就当有 按时间前后切;聚合窗口必须跟着样本时间走
目标编码没做 OOF 0.854 → 0.707 高基数类别排第一,含义说不清 把该列打乱重跑,分数没掉多少 = 它本来没信息 OOF 编码 + 低频平滑,编码器进 Pipeline
预处理先于切分 0.984 → 0.849 不平衡数据上假阳假阴同时极低 train_test_split 在不在所有 fit 之前 切分永远第一步;测试集绝不重采样
分组泄漏 0.706 → 0.564 ⭐ 对老用户还行、对新用户明显差 len(set(uid_tr) & set(uid_te))期望 0 按业务实体切(GroupKFold),不按行切
重复样本跨集 0.745 → 0.605 复杂模型比简单模型好得离谱 哈希查重 + 查近似重复 去重先于切分;文本 MinHash、图片 pHash
代理变量 💀 0.970 → 0.686 去掉一两个特征就崩 单特征 AUC >0.85 全部人工过一遍 删掉或查清写入时刻;契约登记 produced_at
外部数据带未来 症状同 ① 加了外部数据离线大涨、线上不涨 按样本时间分段看提升,泄漏的提升在老样本上更大 只用能给 snapshot_date 的源

七种一起犯:离线 5 折 CV 0.977 → 诚实评估 0.573,落差 0.404(三个种子 0.406/0.403/0.391)。单独一种值 0.13–0.15,七种叠起来是 0.40 —— 互相放大,不是相加;「离线 0.97、线上 0.6」通常不是一个大错,是同时犯了五六个小错。 💀 最毒的代理变量是「人工审核结论」这类字段 —— 几乎就是标签的另一个副本,而名字往往看不出来(status_codeflag_3)。

⭐⭐ 两条元规律:①修泄漏会让线下分数变难看,而且是必然的(OOF 修好后 CV 0.854→0.707,holdout 0.577→0.726)—— 团队按 CV 考核时,这个修复在流程上就不可能发生。这是组织问题,不是技术问题。 ②CV 折间标准差极小是泄漏的指纹 —— 泄漏在每一折里都同样存在,"非常稳定"的意思是"每一折都错得一样多";判断稳定性要看不同切分方式之间的差异。

   上线前四条门禁(任何一条不过就拦住):
   ① 切分体检三个数字全为 0(时间重叠 / 跨集组 / 跨集重复行)
   ② 单特征 AUC 榜首 < 0.85,否则必须附一份字段说明
   ③ 随机切分 AUC 与按时间切分 AUC 的差 < 0.03            ⭐⭐ 最有价值
   ④ 所有有状态变换都在 Pipeline 里
   ⭐ ③ 最值钱:它不需要你知道泄漏在哪,两次实验就能回答
      "我的评测方式本身可不可信"

🎯 偏差

你的训练数据只包含被某个旧系统放行过的样本,而你要预测的是所有人。 ⭐ 弹孔故事的重点不是"反着想",而是返航飞机的数据本身完全正确 —— 它能通过前面十章的全部质量检查。

场景 你有标签的是谁 看不见的那块
招聘 入职并干满一年的人 被简历筛掉的人本来能不能干好
风控 审批通过并放款的人 被拒绝的人本来会不会还钱
推荐 被曝光过的物品 从没被推过的物品会不会被点

⚠️ 这叫反事实缺失 —— 多攒几个月数据没用,多攒的每一条还是旧系统放行的。

情况 只用通过样本训练 上帝视角 缺口
A:老策略只用可观测量(在全体申请人上) 0.821 0.821 0
A:同上,但在通过人群上评估 0.761 ⚠️ 区间截断,偏低
B:旧策略用了你看不到的东西 0.759 0.778 0.019
C:非线性 + 区间被切断(全体上) 0.813 0.841 0.028
C:同上,在被老策略拒掉的那段人群上 💀 0.772 0.829 0.057

⚠️ 区间截断:老策略把最好分的人拒掉了,剩下全是边缘案例 —— 「我们模型 AUC 只有 0.76」这句话在有选择的人群上没有意义。 💀 在你最需要它的地方模型差 0.057 —— ⭐ 你部署新模型的全部意义,正好落在它最不可靠的那一段上。

情况 无随机流量 5% 随机流量 上帝视角 补回缺口
A 0.821 0.821 0.821 —(本来没缺口)
B 0.759 0.770 0.778 58%
C(被拒人群上) 0.772 0.819 0.829 83% ⭐⭐

代价:全体好客户 33.1%/通过人群 54.2%/被拒人群只有 19.0% —— 随机放行被拒的 5%,额外坏账敞口约 6.1%。⭐ 压成本:给最低额度/⭐⭐ 在紧邻阈值那一段分层抽样(1% 敞口 ≈ 5% 均匀抽样的信息量)/时间稀释。 IPWE[ℓ] ≈ (1/N) · Σ s_i / π(x_i) · ℓ_i。💀 死穴是正性假设 —— 硬阈值拒绝的区间里 π 严格等于 0,任何加权都无中生有不出信息。⭐ 随机流量的作用就是把 π=0 变成 π=0.05 —— 它买的不是样本量,是"正性"本身。 ⭐⭐ 权重必须截断并看有效样本量 (Σw)²/Σw²低于实际样本量 30% 就别信这个结论。 拒绝推断四种:全部当坏客户(❌ 把老策略硬编码进新模型)/软标签(⚠️ 自证预言)/IPW(✅ 卡在正性)/随机流量(✅✅ 唯一产生新信息的,也是唯一有成本的)。💀 前两种会让离线指标变好看 —— 只会复制老策略的模型,在老策略的地盘上永远看起来很准。 ⚠️ 选择偏差会自我加强:招聘案例 4 个月学校分布熵 3.1 → 2.2(−29%)。⭐ 评审新增一问:「哪些人不会出现在你的训练数据里?」


🧬 合成数据判断表

万能判据:先问「这批数据里的信息是从哪来的」。答不上来,就别让它影响任何决策。 增强来自不变性先验/仿真来自你写的规则/统计生成来自那份真实数据本身(不产生新信息)/大模型生成来自预训练语料(唯一真引入外部信息,也唯一说不清引入了什么)。 ⚠️ 用 1 万条真实数据采出 100 万条,有效信息还是那 1 万条,但置信度会涨 —— 这是过拟合的一种伪装形式。

先问这个 如果是 该做
它会进评测集吗? 💀 。除非它是单独报分的边界样例清单
信息从哪来,说得清吗? 说不清 ⚠️ 只能用在压测,不能进模型
模型自己的输出回灌吗? ⚠️ 累积不替换随机抽样不按置信度、上多样性监控
做了质量过滤/只留高分吗? 做了 💀 坍塌加速器,一代掉 44% 有效模态数
各分组合成占比 vs 真实占比 差 >10% ⚠️ 生成器在锐化,先修生成器
合成标注有人核过吗? 没有 ⚠️ 抽 200–300 条人核,报人机一致率(<0.75 整类退回)
退役日期吗? 没有 ⭐ 现在就定:真实数据到多少条时降权、清零
监控多样性吗? 不监控 ⭐⭐ 有效簇数 + 尾部占比;只看方差是看不见坍塌的

五个有用场景:隐私替代/长尾补充(⭐ 封顶 ≤ 真实样本量 × 3~5)/冷启动(必须带退役日期)/压测造数(最安全:不进模型只进管道,而且要造形态不是造数值)/边界样例(ROI 最高)。 ⭐ 边界样例是唯一可以进评测集的合成数据 —— 它是一份人工维护的必答题清单,必须单独报分,绝不能被平均进总体 F1

模型坍塌实跑 数字
一维高斯纯自消费,方差每代乘 (n−1)/n,十代后 n=1000 剩 0.9904/n=100 剩 0.9012/n=30 剩 0.7073
方差减半需要多少代 n=1000 约 693/n=100 约 69/n=30 约 20 代
均值无方向随机游走(n=100) 第 0 代 0.101 → 第 10 代 0.325
八模态长尾,十代后 标准差只掉 2.5%有效模态数 −9.9%、尾部 3 模态 −11.5%
💀 加一步「质量过滤」,一代之内 尾部 3 模态占比 9.96% → 0.02%,长尾清零
同上,十代后 标准差 2.625 → 0.268,有效模态数 5.87 → 1.19
刹车:每代掺 30% 真实数据/累积(真实数据永不出池) 熵 1.730 / 1.753(十代只掉 1.0%)

⭐⭐ 坍塌的全部就是两句话:分布宽度单调收缩,中心无方向漂移 —— 一个越来越自信、越来越偏的分布,而它自己看不出来。⚠️ 别被 n=1000 安慰到:真实系统的"每代样本量"是模型在那个子区域见过的独立样本数,长尾常常只有几十。 ⭐ 坍塌先发生在长尾,最后才反映到总体方差 —— 等方差报警时稀有类已经死很久了。 💀 真正的触发条件不是"用了合成数据",是"用合成数据把真实数据换掉了" —— 替换和追加性质完全不同。质量过滤为什么是加速器:似然分数衡量「这条有多像我已经学会的东西」,按它筛选等于每代主动把分布边缘剪掉一圈 —— 你以为在提质量,实际在做定向的多样性删除


🗂️ 版本与血缘

   模型 = f( 代码 , 参数 , 环境 , 数据 )
           ✅      ✅      ✅      ❓
          commit  yaml   镜像    ← 一张每天 INSERT OVERWRITE 的表

   💀 这四项是【短板效应】不是加权平均:前三项 100 分,
      有一项不可回溯,总分还是 0

manifest 必记项_MANIFEST.json,和产出物放在同一个目录):

必须记 为什么
上游表 + 分区/快照 ID 最基本的一条
每个上游的指纹 ⭐⭐ 💀 只记表名不够 —— 分区名没变但内容变了是最常见的死法
产出代码的 commit / 参数 随机种子最常被漏掉
过滤条件 写在 SQL 里,但没人会读三个月前的 SQL
数据截止时间(不是任务运行时间)⭐⭐ 💀 任务重跑三次运行时间不同、截止时间相同
行数/各列空值率 / 触发方式与执行人 指纹对不上时先看这两个

manifest 写在产出物旁边,别写在另一个系统里 —— 💀 血缘系统的存活率比数据文件低得多。 逻辑指纹四个坑:浮点(先 round)/列顺序(按列名排序)/时区(统一 UTC)/行顺序(用 XOR 这类可交换聚合)。⭐⭐ 一定要存 per_column —— 总指纹只说"变了",它说"哪一列变了",这是排查从两天缩到十分钟的差别。

方案(200 GB 表、日变化率 5%) 一年存储 主要痛点
全量快照 ≈ 73 TB 💀 贵到通常撑不过三个月就被清理
时间戳分区 ≈ 1.1 TB 💀 回填会静默改写历史分区(多数团队的默认答案)
内容寻址哈希 ≈ 3.8 TB 需要一层元数据服务
DVC 类工具 同上 大文件性能一般;团队得真的会用 git

⭐ 选型先算日变化率:<2% 内容寻址几乎免费/2–20% 分区 + 每月全量快照/>20% 说明口径不稳定,那不是存储问题。 💀 回填是数据侧最危险的动作,因为它长得像修 bug —— ⭐ 纪律:任何 backfill 都必须写新分区或新版本号,永不原地改写。

分级 做什么 成本
L0(1–3 人) 分区永不覆盖 + 上游分区名打进日志 几乎为零
L1(3–10 人)⭐⭐ _MANIFEST.json:分区名 + 指纹 + 行数 + commit + 参数 半天工程量,解决 80% 的问题
L2(生产/受监管) 正式发版做内容寻址快照;保留期对齐追溯期;季度复现演练 存储 + 一条流程
L3(数十个数据集) 上 DVC / lakeFS / Delta,接血缘平台 一个人长期维护

⭐⭐ L1 是绝大多数团队应该停下来的地方。复现校验不能看聚合指标:AUC 差 0.0023 是"没差别",同一个人的分数差 0.13 是"完全不同的决策" —— 抽 1000 条比对新旧打分,|Δscore| > 0.02 的比例 >1% 就算复现失败。


🏪 特征存储该不该上

⭐⭐ 它解决的是「同一个特征在四个地方被算了四遍」,不是「你的特征不够好」。它是纪律工具,不是能力工具。

   每条符合记 1 分:
   [ ] 线上同时跑着 ≥ 10 个模型
   [ ] 有【在线实时】推理服务(不是只有离线批量打分)
   [ ] 特征复用率 > 50%
   [ ] 过去半年出过 ≥ 2 次训练/推理不一致的线上事故     ⭐ 权重最高
   [ ] 有 ≥ 2 个团队在共享特征
   [ ] 需要严格的 point-in-time 回溯(金融、风控、受监管)
   [ ] 有至少 1 个人可以长期负责它的运维
   [ ] 特征总数 > 300 且还在快速增长

   0–2 分 ⭐⭐ 不要上,用三条轻量替代,一周搞定
   3–5 分 ⚠️ 先只做 point-in-time join + 特征定义 YAML
   6–8 分 ✅ 可以上,但先落实"有人维护"那一条

⭐⭐ 它是为已经发生过的疼痛买的保险。没疼过就买,你买到的只是一个新的运维负担。 ⚠️ 「特征复用」最常被当理由也最常落空<20% 别拿它当理由,>50% 才真省人力,而现实中大多数团队不到 15%。 它不解决:不会让特征变好/⭐⭐ 不会替你定义口径/不会自动发现漂移(存值不判断值)/不解决数据质量。 新增四个问题:多一个故障点(SLA 要求比模型服务更高)/在线存储成本(1000 万实体 × 200 特征 实际约 50 GB Redis)/💀 特征死不掉/调试链路变长。 ⚠️ 延迟预算:模型服务 P99 约 100 ms,特征取数必须压在 10–20 ms —— 这是一次取 200 个特征的 P99,没按真实批量压测就上线是最常见的翻车点三条轻量替代(约一周,拿八成价值):① point-in-time join 库函数 /② 特征定义 YAML + 两个生成器 /③ 一张 online 表 + 单向同步,绝不双写。💀 Feature Store 是个大工程;point-in-time 正确性是个小函数 —— 别因为买不起前者,就连后者也不做。_feature_age_h 判读:中位数≈批周期的一半(日批约 12 小时);出现负数 = 穿越,立刻停;大量 NaN 说明线上会走默认值,训练时也必须走默认值。


🔒 隐私脱敏

是什么 例子
直接标识符 一个字段就能指到人 姓名、手机号、身份证、设备 ID
准标识符 单独无害,组合起来是指纹 生日、性别、邮编、职业、科室、机型
敏感属性 攻击者最终想要的结果 诊断、收入、负债、取向

💀 重识别攻击的 join 钥匙永远是第 ②层,这条链根本不经过第 ①层 —— 你在直接标识符上做的删除、哈希、加密,对它毫无影响

保留的准标识符组合(296,000 条实跑) 唯一率 k<5 占比 最小等价类
生日(到天) + 性别 + 邮编5位 94.9% 💀 100.0% 1
出生年 + 性别 + 邮编5位 6.6% 38.0% 1
5岁分箱 + 性别 + 邮编前3位 0.0% 0.0% 18
生日(到天) + 性别(删掉邮编) 0.2% 14.7% 1

⭐⭐ 只把生日从「到天」粗化成「到年」,唯一率就从 94.9% 掉到 6.6% —— 一个字段的粒度值 88 个百分点。风险来自组合,不是某一列 —— 任何只按列打勾的检查表都抓不到它。

方法 还能 join 吗 ✅ 防住 ❌ 防不住
删除整列 基于该列的一切攻击 其他列的组合攻击;代价最大
掩码 138****5678 ⚠️ 部分 肉眼浏览、截图外泄 💀 剩余空间只有 10⁴
确定性哈希 肉眼直接读出 💀 枚举 + 频率分析
加盐哈希(全局盐) 外部彩虹表 ⚠️ 盐泄漏就退化;频率分析照样有效
Tokenization/每行盐 ❌ 跨表失效 频率分析、枚举 映射表成了新的高价值资产
泛化/分箱 ✅ 粗粒度 重识别(真正有效) 敏感属性的同质推断
加噪 单条精确读数 💀 重复查询取平均能消掉

💀 确定性哈希两个死法① 空间可枚举 —— 纯 Python 单核 1.13 M hash/s,扫完手机号全空间 4.9 小时,一张消费级 GPU:手机号 0.95 秒、身份证 6.1 秒(⭐ 哈希不是加密,它无密钥且公开);② 分布原样保留 —— 哈希前后频率向量逐位完全相同,按频率排序就能映射回去,一个哈希都不用破。 ⭐ 这就是 密码学 07 工作模式 那只 ECB 企鹅:确定性脱敏保护的是"值",泄漏的是"结构",而重识别要的从来就是结构。 🧂 盐就是密钥(不能和数据存一起/必须能轮换/全局盐才能跨表 join/写死 "salt" 等于没加)—— 按 密码学 19 密钥管理 那套来。

泛化代价(k-匿名,取最小值不是平均值) 精确年龄 5 岁分箱 ⭐⭐ 10 岁分箱 20 岁分箱 整列删掉 💀
AUC 0.7345 0.7342 0.7328 0.7275 0.5817
相对损失 −0.0003 −0.0017 −0.0070 −0.1528

⭐⭐ 两张表放一起就是全节的结论:5 岁分箱 + 邮编前 3 位让唯一率 94.9% 归零(最小等价类 18 人),AUC 只掉 0.0003;💀 "不确定就删列"掉 0.1528,是分箱代价的 500 倍 —— 脱敏的默认动作应该是「降分辨率」,不是「删列」。 k-匿名三个漏洞同质攻击(满足 5-匿名的 62.0% 记录里仍有 3.8% 落在"某诊断占 ≥80%"的等价类 —— ⭐ k 保证"认不出是哪个人",不保证"猜不到他的秘密";补丁是 l-多样性/t-接近)/背景知识/💀 多次发布交叉(两份各自 5-匿名的数据交起来 k 可能是 1)。 发布门禁min k ≥ 10 且 min l ≥ 3,并用一份真实的公开名单跑一次重识别演练

ε(Δf=1) 误差中位数/95 分位 分组人数(ε=1) 相对误差中位数/95 分位
0.1 6.86/30.03 3 23.0%/101.2% 💀
1.0 0.70/3.02 12 5.8%/25.2%
2.0 0.35/1.47 50 1.3%/5.8%
10.0 0.07/0.30 5000 0.0%/0.1%

DP 对大盘几乎免费,对小分组毁灭性 —— 而小分组恰恰是罕见病患者、少数族裔、小城市用户。 💀 "我加了随机噪声"不等于 DP:差分攻击实跑(真值 41 vs 排除张三的 40,每次独立加 ε=1 噪声),重复取平均 —— 1 次判对 62.9%、100 次 99.4%、200 次 100.0%。 ⭐⭐ DP 不是"加噪声"这个动作,是"预算"这个制度:①同一个查询必须缓存并返回同一个答案 ②全部 ε 累加记账,账花完就拒绝服务。 留存:原始请求日志 7–30 天/脱敏后行为事件 12–24 个月/训练数据快照 = 模型下线 + 6 个月/标注结果长期但必须能按人删。💀 一条数据的七个藏身处:主库/数仓历史分区快照/特征存储在线+离线表/⚠️ 备份与冷归档(漏得最多)/下游导出/含请求体的日志/向量索引。

已训进模型怎么删(SISA 实跑) AUC 单次删除的重训成本
单一全量模型 0.7168 100%
SISA,4 分片 0.7162(−0.0006 25%
SISA,8 分片 0.6982(−0.0185) 12.5%
SISA,16 分片 0.6987(−0.0181) 6.2%

⭐ 从 4 片到 8 片成本再减半,精度却掉了 30 倍 —— 分片数不是越多越好。⭐⭐ 可遗忘性是一个架构属性,不是一个事后操作 —— 等删除请求来了才想办法,你只剩"全量重训"和"装作没这回事"两个选项。 ⭐ 有人说"我们已经匿名了",就问一句:「最小等价类是多少人?」 答不上来就是没做。


🔨 代码速查

每段自带 import,可以直接复制。

1️⃣ 极端值的正样本富集倍数 | 任何会丢弃数据的地方之前

import numpy as np

def outlier_label_profile(x, y, q=0.999):
    """x: 待检查的数值特征  y: 0/1 标签"""
    x, y = np.asarray(x, float), np.asarray(y, int)
    hi = x > np.nanquantile(x, q)
    base = float(y.mean())
    ext = float(y[hi].mean()) if hi.sum() else float("nan")
    # ⭐ 富集倍数 > 3 = 极端值就是你的信号,任何"删除"都要重新论证
    return {"极端值条数": int(hi.sum()), "总体正样本率": round(base, 5),
            "极端值正样本率": round(ext, 5),
            "富集倍数": round(ext / base, 2) if base > 0 else float("nan")}

2️⃣ 按小时找整点位移 | 怀疑时区、怀疑上报时机变了

import numpy as np

def hour_shift(hours_now, hours_ref):
    """hours_*: 事件发生的小时(0-23 整数数组)"""
    a = np.bincount(np.asarray(hours_ref) % 24, minlength=24).astype(float)
    b = np.bincount(np.asarray(hours_now) % 24, minlength=24).astype(float)
    a, b = a / a.sum() + 1e-9, b / b.sum() + 1e-9
    psi = float(np.sum((b - a) * np.log(b / a)))
    cor = [float(np.corrcoef(a, np.roll(b, -s))[0, 1]) for s in range(24)]
    k = int(np.argmax(cor))
    # ⭐ 实跑:整体 +8h 时按天 PSI≈0、按小时 PSI=1.457,位移 8 的相关系数 1.000
    return k, round(cor[k], 3), round(psi, 3)

3️⃣ 缺失机制探针 | 决定怎么填之前

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

def missing_probe(X_obs, is_missing, y=None):
    """用已观测特征预测「这一行会不会缺」。⭐ 加上 y 之后 AUC 暴涨 = MNAR 强信号"""
    def auc(M):
        lr = LogisticRegression(max_iter=2000).fit(M, is_missing)
        return round(float(roc_auc_score(is_missing, lr.predict_proba(M)[:, 1])), 3)
    a1 = auc(np.asarray(X_obs))
    a2 = auc(np.column_stack([X_obs, y])) if y is not None else None
    # 实跑:MCAR 0.505/0.506 | MAR 0.829/0.829 | MNAR 0.642/0.837
    return a1, a2      # ⚠️ 它只能证伪,不能证明

4️⃣ 单特征 AUC 排序 | 抓代理变量最快的一段

import numpy as np
from sklearn.metrics import roc_auc_score

def single_feature_auc(X, y, names, warn=0.85):
    """⭐ 任何单特征 AUC > 0.85 都要人工过一遍它的【写入时刻】"""
    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 = float(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])

5️⃣ 切分体检 | 三个边界一次查完,都必须是 0

import hashlib
import numpy as np

def split_audit(X_tr, X_te, t_tr=None, t_te=None, g_tr=None, g_te=None):
    """时间重叠 / 组重叠 / 重复样本跨集。⭐ 任何一个 > 0,就先别看指标了"""
    rep = {}
    if t_tr is not None:
        rep["时间重叠"] = float(np.max(t_tr) - np.min(t_te))
    if g_tr is not None:
        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))
    return rep

6️⃣ 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):
    """⭐ 训练集用折外值,测试集用全训练集映射;k 是平滑强度(10~50)"""
    cat_tr, y_tr = np.asarray(cat_tr), np.asarray(y_tr, dtype=float)
    prior = float(y_tr.mean())

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

    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)
    # ⚠️ 修好之后 CV 分数一定会掉。掉下去的那部分本来就不是你的
    return oof, np.array([mp_all.get(v, prior) for v in np.asarray(cat_te)])

7️⃣ point-in-time join | 生成任何带时间的训练集

import pandas as pd

def point_in_time_join(samples, features, entity="uid",
                       sample_ts="event_time", feat_ts="available_time"):
    """⭐⭐ features 的时间列必须是【可用时间】,不是【业务时间】"""
    out = pd.merge_asof(
        samples.sort_values(sample_ts), features.sort_values(feat_ts),
        left_on=sample_ts, right_on=feat_ts, by=entity,
        direction="backward",           # ⭐ 只向过去找,绝不向未来找
        allow_exact_matches=False,      # ⭐⭐ 同一时刻不算"已可用",宁可保守
    )
    out["_feature_age_h"] = (out[sample_ts] - out[feat_ts]).dt.total_seconds() / 3600
    # ⭐ 有负数 = 穿越,立刻停;中位数应约等于批任务周期的一半
    return out

8️⃣ 按实体切分 + 跨集泄漏探针 | 切分之前

import numpy as np

def entity_split(entity_ids, test_size=0.3, seed=0):
    """⭐⭐ 同一实体的所有记录只能落在同一侧;去重必须先于切分"""
    ids = np.asarray(entity_ids)
    uniq = np.unique(ids)
    rng = np.random.default_rng(seed)
    rng.shuffle(uniq)
    test_ids = set(uniq[:int(len(uniq) * test_size)].tolist())
    is_test = np.array([i in test_ids for i in ids])
    return ~is_test, is_test

def leak_probe(g_train, g_valid):
    """验证集里有多少行在训练集有同实体副本。⭐ 期望是 0,不是「比较小」"""
    return float(np.isin(np.asarray(g_valid), np.unique(g_train)).mean())

9️⃣ Kappa + 正类专属一致率 | 看到「同意率 90%」的时候

import numpy as np
from sklearn.metrics import cohen_kappa_score

def kappa_report(a, b, c, d):
    """a=两人都标负  b=A负B正  c=A正B负  d=两人都标正"""
    n = a + b + c + d
    po = (a + d) / n
    p1, p2 = (a + b) / n, (a + c) / n
    pe = p1 * p2 + (1 - p1) * (1 - p2)                  # ⭐ 偶然一致率
    y1 = np.array([0] * a + [0] * b + [1] * c + [1] * d)
    y2 = np.array([0] * a + [1] * b + [0] * c + [1] * d)
    return {"同意率Po": round(po, 4), "偶然一致Pe": round(pe, 6),
            "Kappa": round(float(cohen_kappa_score(y1, y2)), 4),
            # ⭐ 不平衡数据必须同时看这一行,它才是真实水平
            "正类专属一致率": round(2 * d / (2 * d + b + c), 4)}

print(kappa_report(883, 50, 50, 17))    # Po=0.900,而 Kappa 只有 0.2001

🔟 逻辑指纹 | 每次产出训练集都算一次,写进 manifest

import hashlib
import json
import numpy as np
import pandas as pd

def logical_fingerprint(df, float_dp=6):
    """回答「这两份数据是不是同一份」,而不是「这两个文件是不是同一个文件」"""
    parts = []
    for c in sorted(df.columns):                        # ⭐ 列顺序无关
        s = df[c].round(float_dp) if pd.api.types.is_float_dtype(df[c]) else df[c]
        h = pd.util.hash_pandas_object(s, index=False).to_numpy(dtype=np.uint64)
        parts.append((c, int(np.bitwise_xor.reduce(h)))) # ⭐ XOR 聚合 → 行顺序无关
    payload = {"n_rows": int(len(df)), "cols": parts}
    digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
    # ⭐⭐ per_column 才是救命的那一半:总指纹只说"变了",它说"哪一列变了"
    return {"fingerprint": digest[:16], "n_rows": len(df), "per_column": dict(parts)}

📏 关键数字与阈值

主题 数字
时间账 14 周项目:数据相关 51 人日 = 74%,模型选型 4 人日 = 6%(第 1 天就定了)
换模型 vs 修数据 11 天 +0.019 vs 13 天 +0.065 —— ⭐ 收益 3.4 倍
标签噪声实验 换模型差 0.013;修 10% 噪声 0.019~0.029;GBDT 比 RF 掉得多(−0.0285
缺量还是缺质 ⭐ 训练集随机删一半,指标掉 <0.005 = 你不缺量,缺质量
疑似哨兵值 某个具体数值占比 >20%(连续变量则任何值 >1%)
疑似泄漏(单特征) 与标签相关 |r| >0.8;单特征 AUC >0.85
时区错位 同一份数据:按天 PSI ≈0,按小时 PSI 1.457(位移 8 相关系数 1.000)
3σ 掩蔽效应 上界 3,248 → 混入 50 条错误后 21,530(涨 6.6 倍),只抓到 39/50
重复跨集 验证集 41.5% 有孪生;1-NN 虚高 +0.1624;有孪生部分 AUC 1.0000
传递闭包相变 误判率 5e-4 → 1e-3,最大簇从 23 个实体 → 1471 个(64 倍)
阻塞压缩比 20 万条:城市 1/339,城市+姓氏首字母 1/8842
质量阈值 历史 P99 × 1.5(误报 0 次、命中 30/30);拍脑袋 1% 误报 2 次
缺失指示 一列 0/1 = +0.0285 AUC-999 在线性模型上 0.654 → 0.629
缺失率行动线 >1% 自动生成 _is_missing;填充统计量只从训练折算
Kappa 下限 训练集 ≥0.60评测集 ≥0.80/高风险 ≥0.80 且有仲裁
同意率陷阱 Po 0.900 而 Kappa 0.2001(Pe = 0.874978)
有序标签加权 无权 0.3964/线性 0.6747/二次 0.8510(同一份数据)
质检抽样量 n = ln(1−C)/ln(1−p):2% 缺陷 149 条;抽 200 条全对 = ≤1.49%
试标注收敛 Kappa 提升 <0.05 且新增争议 <3 条/轮,两条同时满足
泄漏落差 单种 0.13–0.15;七种一起 0.977 → 0.573(0.404)
泄漏门禁 ⭐⭐ 随机切 vs 按时间切,差 <0.03
随机流量 5% 在"区间被切断"时补回 83% 缺口;额外坏账敞口约 6.1%
IPW 有效样本量 <实际样本量的 30% 就别信这个结论
合成数据封顶 稀有类合成量 ≤ 真实样本量 × 3~5
模型坍塌 纯自消费十代掉 9.9% 有效模态数;加质量过滤一代掉 44%
复现校验 ⭐⭐ 抽 1000 条,|Δscore| >0.02 的比例 >1% 就算复现失败
日变化率 <2% 内容寻址/2–20% 分区+月快照/>20% 是口径问题不是存储问题
特征取数延迟 P99 10–20 ms(一次取 200 个特征,不是单 key)
k-匿名门禁 min k ≥ 10 且 min l ≥ 3 才允许对外发布
泛化代价 5 岁分箱 AUC 掉 0.0003;整列删掉掉 0.1528(500 倍)
哈希枚举 消费级 GPU:手机号 0.95 秒、身份证 6.1 秒
DP 小分组 ε=1 时 n=3 的相对误差 23.0%,n=5000 是 0.0%
SISA 分片 4 片:成本 25%、AUC 掉 0.0006;8 片掉 0.0185(30 倍)

💀 事故一行版

事故 → 教训
01 换了四次模型,根因是标注按 2023 版规则而线上已是 2024 版 → 45 人日白费,正确修复只要 9 人日;最贵的是那条一直在涨的离线曲线
02 曝光改成「停留 ≥1s」,字段类型全没变 → 正样本率 4.1%→6.2%,下游阈值全失效,19 天。⭐ 「指标变好了」从来不是「不用查」的理由
03 新增枚举被 .get(x, 3) 静默映射成 unknown → 该渠道 AUC 0.548、坏账 216 万,而整体只降 0.007。⭐ 整体指标会稀释局部灾难
04 event_ts 从 UTC 改成本地时间(+8h),灰度 3 周把断崖变成缓坡 → 次日留存 31.2%→35.3%。⭐ 灰度会把故障伪装成漂移
05 一行 fillna(mean) 把 47% 缺失填成 2180.00 → 非一线 AUC 0.583、坏账 4100 万。⭐ 填充在监控之前,监控的就是填充逻辑
06 只做精确去重(0.4%),近重复 3.1% → 离线 0.871/线上 0.642,17 次实验结论全作废。⭐ 共用污染评测集时 A/B 保护不了你
07 3σ 从「截断」改成「删除整行」,只删 0.31% → 累计多损失 380 万。⭐ 模型按笔数优化,损失按金额结算
08 六天的分区都是同一份拷贝,38 条断言 38 绿(行数波动 0.00%)→ 多核销 430 万。⭐ 盲区不是偏差太大,是偏差为零
09 32 类互斥、0 条边界样例、没做试标注 → 「投诉↔退款」一致率 51%41 万 + 延期 5 周。⭐ 根因是把正交属性塞进了互斥类别表
10 评测集单人标注("口径最统一")→ Fleiss 只有 0.38,用仲裁金标重测结论完全翻转62 万
14 「5 折 CV 折间标准差只有 0.004」被当成可靠证据 → 坏账 2900 万。⭐ 一致性恰恰是泄漏的指纹
15 93.2% 没进面试的投递全标成负样本 → 模型学的是「HR 会不会点开简历」,真实 AUC 0.71 而非 0.88
16 同一套 prompt 造训练集和评测集 → 离线 0.94、真实 0.61,兜底率 8%→27%。⭐ 出的题和做的题来自同一支笔
17 backfill 原地改写 + 保留期 90 天 < 追溯期 24 个月 → 省下的 5000 元最后花掉 400 多万
18 特征取的是"截至跑数当天" → 离线 0.83/线上 0.68,210 万。⭐ 最反直觉的是线上是对的、离线错了
19 删了姓名手机号身份证就叫"已匿名" → 87.3% 的人被指名道姓对上。⭐ 纪要把「已删除直接标识符」写成了「已匿名」

📖 术语对照

中文 英文 一句话
数据契约 data contract 把「我们说好了不改」变成「你一改,你的 CI 就红」
哨兵值 sentinel value 0-9991970-01-01不是 NULL,所以缺失率监控看不到
条件随机缺失 MAR 只取决于你已观测到的其他字段
非随机缺失 MNAR 取决于这个值本身任何填充都有偏
阻塞 blocking 实体解析里先分桶再两两比较;只降召回,不降精确
传递闭包 transitive closure A=BB=CA=C;⚠️ 有相变点,会滚雪球
一致性(标注) inter-annotator agreement 「比按同样比例乱按好了多少」,不是「标得一样的比例」
普遍性效应 prevalence effect 同样的同意率,类别越不平衡 Kappa 越低
置信学习 confident learning 用模型的预测概率反过来定位可疑标签(第 11 章
主动学习 active learning 让模型挑下一批标什么;⚠️ 有冷启动和采样偏差两个陷阱
评测集污染 contamination 评测样本以某种形式已经被模型见过(第 13 章
数据泄漏 data leakage 训练时用了「预测那一刻拿不到」的信息
代理变量 target leakage 一个只有事后才存在的字段,几乎是标签的副本
区间截断 range restriction 最好分的那批人被筛掉了,剩下的当然更难分
反事实缺失 counterfactual missing 「如果当初批了他会怎样」,数据库里永远没有答案
拒绝推断 reject inference 被拒绝的样本永远没有标签,怎么补
逆倾向加权 IPW 按「被选中概率」的倒数加权;💀 死穴是正性假设
模型坍塌 model collapse 自消费下分布宽度单调收缩、中心随机漂移
回填 backfill 💀 长得最像修 bug 的破坏性操作
时点正确性 point-in-time correctness 取该样本事件时间之前最后一次可见的特征值
可用时间 available_time 这个值线上真正能被读到的时刻,不是它描述的时刻
准标识符 quasi-identifier 单独无害,组合起来是指纹;重识别的 join 钥匙
k-匿名 k-anonymity 每种准标识符组合至少对应 k 个人;取最小值不是平均值
差分隐私 differential privacy 一个人在与不在,输出分布最多差 e^ε 倍;是制度不是动作

🔗 模型上线之后 · 附录A 速查(姊妹篇:训完之后的那一半)/ Kaggle · 23 数据泄露与外部数据(竞赛视角的同一件事)/ ML 基础 · 05 评估与过拟合(切分与交叉验证的基本功)/ 大模型全景导论 · 11 评测体系("评测集继承同样的歧义"在大模型里的重灾区)

👉 回到首页 / 想亲手跑一遍:第 20 章 实战与挑战项目

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