🏠 总目录📚 本教程 17 · 版本与血缘
📑 本页目录(点开跳转)

17 · 数据版本与血缘

62 分钟 | ⭐ 三个月后,你能重现当时那份训练数据吗


🎯 一句话

只存代码不存数据,等于没有可复现。 模型是 代码 × 数据 × 参数 × 环境 的函数 —— 你把其中三项都版本化了,剩下那一项是一张每天被覆盖的上游表


🔍 一、先花五分钟做个自测

随便挑一个三个月前上线的模型,回答这六个问题。

   [ ] ① 训练用的是哪张表、哪个分区/快照?
   [ ] ② 那份数据现在还在吗?内容和当时【逐行相同】吗?   ⭐ 大多数人卡在这一条
   [ ] ③ 是哪段 SQL 产出的?那段 SQL 从那天起改过吗?
   [ ] ④ 过滤掉了哪些行?(去重规则、时间窗、黑名单)
   [ ] ⑤ 当时的行数是多少?现在重跑是多少?
   [ ] ⑥ 现在重跑一遍,能得到【指标相同】的模型吗?

为什么"我们代码都在 git 里"救不了你:

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

   💀 这四项是【短板效应】,不是加权平均。
      有一项不可回溯,整体就是不可复现 ——
      前三项做到 100% 分,总分还是 0。

本章和《模型上线之后》第 17 章的分工,就在这张图上那边负责左边三格(代码、参数、环境、模型权重),这边只负责最右边那一格。 两边都做齐了才叫可复现;而现实中挂掉的,九成是最右边这一格。


📦 二、四种版本化方案:成本、粒度、适用规模

假设一张 200 GB 的宽表,每天产出一版日变化率约 5%(这是很典型的数值)。

方案 怎么做 一年存储 回溯粒度 适用规模 ⚠️ 主要痛点
全量快照 每次训练前整表拷一份,路径带日期 ≈ 73 TB 💀 完美 表 < 10 GB,或"只在正式发版时存" 贵得离谱,通常撑不过三个月就被清理
时间戳分区 dt= 分区,只写当天增量,永不覆盖 ≈ 1.1 TB 分区级 绝大多数团队的默认答案 💀 回填(backfill)会静默改写历史分区
内容寻址哈希 按内容块算 hash 存对象存储,重复块只存一次 ≈ 3.8 TB 文件/块级 大表 + 需要严格回溯(金融、医疗) 需要一层元数据服务,运维有成本
DVC 类工具 本质是内容寻址 + git 里只存指针文件 同上 文件级 ⭐ 中小团队、实验型项目 大文件性能一般;团队得真的会用 git

怎么选:先算你的日变化率。

   日变化率 = 每天真正变了的字节 / 全表字节

   < 2%    → 内容寻址几乎免费(去重率极高)        ⭐
   2% ~ 20% → 时间戳分区 + 定期全量快照(比如每月 1 号)
   > 20%   → 数据本身就在剧烈变化,
              ⚠️ 先去问"为什么",这通常是口径不稳定,不是存储问题

一条被反复验证的经验绝大多数团队不需要上 DVC / lakeFS / Delta Lake。 他们需要的只是两条纪律 —— ① 分区永不覆盖;② 每次训练把用到的分区名和指纹写进 manifest。 这两条不需要任何新工具,但它们能解决 80% 的复现问题


🔑 三、指纹:用一行字符串锁住一份数据

版本化只解决"数据还在",指纹解决"它还是不是原来那份"

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

def logical_fingerprint(df: pd.DataFrame, float_dp: int = 6) -> dict:
    """逻辑指纹:对文件切分方式、行顺序、分片数都不敏感。
    回答的是"这两份数据是不是同一份",而不是"这两个文件是不是同一个文件"。"""
    cols = sorted(df.columns)                                  # ⭐ 列顺序无关
    parts = []
    for c in cols:
        s = df[c]
        if pd.api.types.is_float_dtype(s):
            s = s.round(float_dp)                              # ⭐ 浮点必须先定精度,否则永远对不上
        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()
    return {
        "fingerprint": digest[:16],          # ⭐ 16 位十六进制足够,写进 manifest 一眼能比
        "n_rows": len(df),
        "n_cols": len(cols),
        "per_column": dict(parts),           # ⭐⭐ 出问题时,这一列能直接告诉你是【哪一列】变了
    }

# ⭐⭐ 关键用法:不要只存总指纹,要存 per_column ——
#     总指纹只能告诉你"变了",per_column 能告诉你"哪一列变了",
#     这是排查时间从两天缩到十分钟的差别

四个让指纹永远对不上的坑:

现象 解法
浮点精度 同样的数据,两台机器算出不同指纹 ⭐ 先 round 到固定小数位再 hash
列顺序 SELECT * 在 schema 变更后顺序变了 按列名排序后再 hash
时区 同一个时间戳存成 naive / aware 两种 统一转 UTC 再 hash
行顺序 分布式任务每次产出顺序不同 ⭐ 用 XOR / 求和这类可交换的聚合

🧾 四、血缘要记什么

血缘不是一张关系图,是一份「能让人照着重跑一遍」的记录。

必须记 为什么
上游表 + 分区/快照 ID 最基本的一条
每个上游的指纹 ⭐⭐ 💀 只记表名是不够的 —— 分区名没变但内容变了,就是最常见的死法
产出这份数据的 SQL / 代码的 commit 逻辑本身也会变
参数(时间窗、阈值、采样率、随机种子) ⭐ 随机种子是最常被漏掉的一个
过滤条件 "去掉了哪些行"往往写在 SQL 里,但没人会去读三个月前的 SQL
数据截止时间(不是任务运行时间)⭐⭐ 💀 任务重跑了三次,运行时间不同但数据截止时间相同 —— 混淆这两个会直接导致误判
行数 / 各列空值率 便宜的健康检查,指纹对不上时先看这两个
触发方式与执行人 排查"这份数据是谁在什么情况下造出来的"

一份 run manifest 长这样(和产出物放在同一个目录,文件名 _MANIFEST.json):

{
  "run_id": "train_credit_v37_20240131_0412",
  "produced_at_utc": "2024-01-31T04:12:38Z",
  "data_cutoff_utc": "2024-01-31T00:00:00Z",
  "code": {"repo": "risk-models", "commit": "9f2c1ab", "entry": "jobs/build_train.py"},
  "params": {"window_days": 180, "neg_sample_rate": 0.15, "seed": 20240131},
  "filters": ["status != 'test'", "apply_time < data_cutoff", "dedup by (uid, apply_date)"],
  "inputs": [
    {"table": "dw.user_profile_wide", "partition": "dt=2024-01-31",
     "fingerprint": "3a91f0c7d2be4155", "n_rows": 8421339},
    {"table": "dw.repay_label", "partition": "dt=2024-01-31",
     "fingerprint": "77c0be14aa39d802", "n_rows": 3110284}
  ],
  "output": {"path": "s3://ds/train/credit_v37/", "fingerprint": "b5e2149c8f0a7731",
             "n_rows": 3110284, "n_cols": 214}
}

把 manifest 写在产出物【旁边】,不要写在另一个系统里。 三个月后你能找到那份 parquet,就一定能找到 _MANIFEST.json; 但你不一定还能登进当年那个血缘平台 —— 它可能换了域名、换了负责人、或者根本已经下线了。 💀 血缘系统的存活率,比数据文件低得多。


🛑 读到这里可以停 —— 前半章讲完了(约 19 分钟)。 后半章还有:只存代码不存数据的两种死法 · 一次真实的事故复盘 · 和《模型上线之后 17》的分工 · 按团队规模分级实施 回来的时候不用重读,直接从下一节接着看就行。


☠️ 五、只存代码不存数据的两种死法

   死法 A:上游被覆盖 —— 最直白的一种
     ├ 表是 INSERT OVERWRITE 的,或者分区保留期只有 90 天
     └ 结果:你连"当时是什么"都拿不到,讨论到此结束

   死法 B:上游"还在",但语义变了 —— 💀 更阴险
     ├ 有人改了口径,并且【回填了全部历史分区】
     ├ 分区名没变、行数没变、schema 没变
     ├ 你重跑得到的结果不一样,但你不知道为什么
     └ ⭐ 没有指纹的话,这个问题【无法被发现,也无法被证明】

💀 回填(backfill)是数据侧最危险的一个动作,因为它长得像修 bug: "我们发现 income_verified 算错了,已经修好并回填了历史数据。" 听起来完全正确 —— 但它同时把所有历史模型的训练数据静默改写了。纪律:任何 backfill 都必须写新分区(或新版本号),永不原地改写。 存储贵一点,比不可复现便宜一万倍。


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

   系统:某消费信贷的授信模型 v37,2 月上线
   10 月,监管问询:
     · 解释 3 月 12 日对某个申请人的【拒绝】决策
     · 证明模型未使用某类禁用字段
   团队手里有:git commit、Docker 镜像 digest、超参 yaml、模型权重文件
   团队手里没有:训练数据快照 ⭐

发生了什么

时间 事件
1 月 31 日 dw.user_profile_widedt=2024-01-31 分区训练,该表保留期 90 天
3 月 数据团队修正 income_verified 的口径:从"近 6 个月流水"改成"近 12 个月",并 backfill 了全部历史分区。分区名没变、行数没变、schema 没变
10 月 监管要求复现。原始分区早已过期被清理,只能用当前口径重建一份"等价数据"
复现结果 重跑模型 AUC 0.7412,原始记录 0.7389;Top-10 特征重要性里 3 个换了位次
💀 关键 对那位申请人,重跑模型给出的分数是 0.44,原始记录是 0.31,审批阈值 0.35 —— 重跑出来的模型会批准他

为什么没被发现

   ① 数据保留期 90 天  <  监管追溯期 24 个月      ⭐ 这两个数字从来没有人放在一起看过
   ② backfill 没有产生新分区,也没有发出任何变更通知
   ③ 团队【有】血缘系统 —— 但它只记了"表名",
      没记【分区指纹】,也没记【口径版本】
      💀 有血缘 ≠ 可复现,这是最容易被忽悠过去的一点
   ④ 每次重训 AUC 本来就在 0.73~0.75 之间波动,
      0.7412 vs 0.7389 的差异【小到不会触发任何告警】
      ⭐⭐ 但它足以让一个个体样本的决策翻转 ——
         聚合指标的稳定,完全掩盖了个体决策的不稳定

代价

   无法自证 → 监管现场检查升级
     专项投入        2 名工程师 + 1 名合规,6 周
     外部咨询/法务    ≈ 86 万元
   模型被要求下线整改 5 周
     期间回退到旧规则引擎,通过率 −11%
     少放款约 2.3 亿元,估算利息收入损失 ≈ 340 万元
   全行 23 个模型补做数据快照
     一次性新增存储 41 TB(年成本约 12 万元)

   ⭐ 对比一下:如果当初就存快照,
      单模型每年存储成本约 5000 元。
      💀 省下的 5000 元,最后花掉了 400 多万。

该补什么

缺失 补上之后
训练数据没有快照 💀 ⭐ 每次正式发版的训练集做一次内容寻址快照(不是每次实验都做)。日变化率 5%,增量存储可控
保留期短于追溯期 ⭐⭐ 建一条硬规则:任何进过生产模型的数据,保留期 ≥ 监管追溯期,并把这条写进数据契约(第 03 章
backfill 原地改写 💀 禁止原地 backfill。改口径必须写新分区或加版本后缀,旧分区冻结
血缘只记表名 manifest 里必须有每个上游的指纹 + 行数 + 数据截止时间,缺一项不许发版
只看聚合指标 ⭐⭐ 复现校验不看 AUC,看个体分数:抽 1000 条样本,比较新旧模型打分,|Δscore| > 0.02 的比例 > 1% 就算复现失败
没有复现演练 每季度随机抽一个线上模型做"复现演练",当作消防演习

⭐⭐ 这个案例最值钱的一句话AUC 差 0.0023 是"没差别",同一个人的分数差 0.13 是"完全不同的决策"。 可复现性不能用聚合指标验证,必须用个体样本逐条比对 —— 因为你要向监管解释的,从来不是平均值,是某一个人。


🤝 七、和《模型上线之后 17》的分工

两章讲的是同一件事的两半,边界很清晰:

模型上线之后 · 17 数据这一关 · 17(本章)
管什么 代码、参数、环境、模型权重、推理链路 数据本身
核心问题 「这个预测是哪个模型版本给出的?」 「这个模型是哪份数据训出来的?」
典型手段 模型注册表、镜像 digest、请求级版本打点 分区不覆盖、内容指纹、run manifest
典型死法 加载了错误的模型版本 💀 上游被覆盖 / 被静默 backfill
谁负责 通常是算法工程 / MLOps 通常是数据工程 —— ⚠️ 所以最容易两边都以为对方管了

⚠️ 这一行"两边都以为对方管了"是真实事故的高发区: 模型侧觉得"数据是数仓的事",数据侧觉得"训练集是算法自己拉的"。 ⭐ 解法很简单:把「上游分区指纹」列进模型卡的必填项 (落地处是本章第四节那份 run manifest 的 inputs[].fingerprint 字段, 以及《模型上线之后》第 19 章的模型卡模板)—— 谁发版谁填,填不出来就不许发版。


🪜 八、按团队规模分级实施

别一上来就上工具,按这个顺序做。

级别 做什么 成本 什么时候升级到下一级
L0(1–3 人) 分区永不覆盖 + 训练脚本把上游分区名打进日志 几乎为零 有第二个人需要复现你的结果时
L1(3–10 人) _MANIFEST.json:分区名 + 指纹 + 行数 + commit + 参数 半天工程量 ⭐⭐ 性价比最高的一级 模型进了生产、有回溯要求时
L2(生产 / 受监管) 正式发版时做内容寻址快照;保留期对齐追溯期;季度复现演练 存储 + 一条流程 数据集数量多到人管不过来时
L3(数十个数据集) 上 DVC / lakeFS / Delta 之类的工具,接血缘平台 一个人长期维护

⭐⭐ L1 是绝大多数团队应该停下来的地方。 一个 _MANIFEST.json 文件、半天工程量, 能解决你 80% 的"三个月后重现不出来"问题。 剩下 20% 才需要工具,而那 20% 的团队自己知道自己是谁。


🔗 这一章连到哪里

去哪 为什么
模型上线之后 17 版本回溯与可复现 ⭐⭐ 成对的另一半:那边管代码/参数/环境/模型权重,这边只管数据。四项缺一不可,是短板效应
模型上线之后 16 重训练 每次重训都在产生一份新的训练数据版本 —— 重训频率越高,manifest 越重要
模型上线之后 19 合规审计与模型卡 ⭐ 案例里监管要的东西。把「上游分区指纹」列进模型卡必填项,就是这两章的接口
AI基础设施 22 数据管线与存储 分区、对象存储、内容寻址的工程实现细节
数据泄漏的七种来源 「数据截止时间 vs 任务运行时间」混淆,在那一章里是一种泄漏,在这一章里是一种不可复现

✅ 检查点

  1. 五分钟自测的六个问题是什么?大多数团队卡在哪一条?
  2. 为什么说"代码都在 git 里"救不了可复现?这四项是什么关系?
  3. 四种版本化方案在「200 GB 表、日变化率 5%」下一年各要多少存储?各自的主要痛点是什么?怎么用日变化率选方案、超过 20% 说明什么?
  4. 逻辑指纹为什么要按列名排序、要 round 浮点、要用 XOR 聚合?为什么一定要存 per_column 而不只存总指纹?
  5. 血缘里最容易漏的三项是什么?为什么 manifest 要放在产出物旁边而不是血缘平台里?
  6. 只存代码不存数据的两种死法是什么?为什么说 backfill "长得像修 bug"?纪律是什么?
  7. 信贷那个事故里,重跑的 AUC 和原始差多少?那位申请人的分数差多少?为什么说前者"没差别"而后者"完全不同"?
  8. 那个事故的四条"没被发现"的原因里,哪一条最容易被忽悠过去?代价一共多少、如果当初就存快照要花多少?
  9. 本章和《模型上线之后 17》怎么分工?为什么这个边界特别容易出事、怎么解决?
  10. 分级实施表里,哪一级性价比最高、要多少工程量、能解决多少问题?
👀 答案
  1. ①用了哪张表哪个分区 ②那份数据现在还在吗、内容逐行相同吗 ③是哪段 SQL 产出的、改过吗 ④过滤掉了哪些行 ⑤行数是多少 ⑥现在重跑能得到指标相同的模型吗。⭐ 大多数人卡在第 ②条 —— 上游表是 INSERT OVERWRITE 的,或者保留期太短。
  2. 因为 模型 = f(代码, 参数, 环境, 数据),前三项 git/yaml/镜像都解决了,数据那一格是一张每天被覆盖的表。⭐ 这四项是短板效应,不是加权平均 —— 前三项做到 100 分,有一项不可回溯,总分还是 0
  3. 全量快照 ≈ 73 TB(贵到撑不过三个月就被清);时间戳分区 ≈ 1.1 TB(💀 痛点是 backfill 会静默改写历史分区);内容寻址 ≈ 3.8 TB(需要一层元数据服务);DVC 类同内容寻址(大文件性能一般,且团队得真的会用 git)。选型看 日变化率 = 每天真正变了的字节 / 全表字节:<2% 内容寻址几乎免费;2%–20% 时间戳分区 + 每月一次全量快照;>20% 说明数据本身在剧烈变化,通常是口径不稳定,先去问"为什么",这不是存储问题
  4. 排序列名 → 对 SELECT * 的列顺序变化免疫;round 浮点 → 否则两台机器算出的指纹永远对不上;XOR 这类可交换聚合 → 对分布式任务的行产出顺序免疫。⭐⭐ 必须存 per_column,因为总指纹只能告诉你"变了",per_column 能告诉你"哪一列变了" —— 这是排查从两天缩到十分钟的差别。
  5. 最容易漏的三项:过滤条件(写在 SQL 里但没人会去读三个月前的 SQL)、数据截止时间(不是任务运行时间,任务重跑三次运行时间不同但截止时间相同)、随机种子。manifest 要放产出物旁边(_MANIFEST.json),因为💀 血缘系统的存活率比数据文件低得多 —— 它可能换域名、换负责人或已经下线。
  6. A:上游被覆盖(OVERWRITE 或保留期到期),连"当时是什么"都拿不到;B:上游还在但语义变了(口径改动 + 回填历史),分区名/行数/schema 全没变,⭐ 没有指纹的话这个问题无法被发现也无法被证明。backfill 长得像修 bug 是因为它的说法是"我们发现算错了,已修好并回填历史" —— 听起来完全正确,但它同时静默改写了所有历史模型的训练数据。纪律:⭐ 任何 backfill 必须写新分区或新版本号,永不原地改写。
  7. AUC 0.7412 vs 0.7389,差 0.0023;那位申请人的分数 0.31 → 0.44,阈值 0.35,差 0.13,决策直接翻转(重跑出的模型会批准他)。⭐⭐ 因为重训 AUC 本来就在 0.73–0.75 之间波动,0.0023 小到不会触发任何告警,但足以翻转个体决策 —— 聚合指标的稳定完全掩盖了个体决策的不稳定。所以复现校验要看个体分数:抽 1000 条比对,|Δscore| > 0.02 的比例 > 1% 就算复现失败
  8. 最容易被忽悠过去的是第 ③条 —— 团队"有"血缘系统,但它只记表名,不记分区指纹和口径版本;💀 有血缘 ≠ 可复现。代价:6 周专项(2 工程师 + 1 合规)、外部咨询法务 ≈ 86 万、模型下线整改 5 周期间通过率 −11%、少放款 2.3 亿、利息损失 ≈ 340 万、23 个模型补快照新增 41 TB。而当初存快照单模型每年约 5000 元 —— 💀 省下的 5000 元最后花掉了 400 多万
  9. 《模型上线之后 17》管代码、参数、环境、模型权重、推理链路,回答"这个预测是哪个模型版本给的";本章只管数据本身,回答"这个模型是哪份数据训出来的"。容易出事是因为⚠️ 模型侧觉得数据是数仓的事、数据侧觉得训练集是算法自己拉的,两边都以为对方管了。⭐ 解法:把「上游分区指纹」列进模型卡的必填项,谁发版谁填,填不出来不许发版。
  10. L1 性价比最高:加一个 _MANIFEST.json(分区名 + 指纹 + 行数 + commit + 参数),半天工程量,能解决 80% 的"三个月后重现不出来"问题。⭐⭐ L1 是绝大多数团队应该停下来的地方 —— 剩下 20% 才需要 DVC/lakeFS 这类工具,而那 20% 的团队自己知道自己是谁。

🛑 可以停在这里

走神救援

⭐⭐核心:只存代码不存数据,等于没有可复现。 模型 = f(代码, 参数, 环境, 数据)——前三项有 git commit、yaml、镜像 digest,数据那一格是一张每天 INSERT OVERWRITE 的表;💀这四项是短板效应不是加权平均,前三项 100 分、有一项不可回溯,总分还是 0。先做五分钟自测:用了哪张表哪个分区 / 那份数据现在还在吗、内容逐行相同吗(⭐大多数人卡在这条)/ 哪段 SQL 产出的、改过吗 / 过滤掉了哪些行 / 行数多少 / 现在重跑指标一样吗。📦四种版本化方案(200 GB 表、日变化率 5%):全量快照 ≈ 73 TB/年(完美但贵到撑不过三个月)、时间戳分区 ≈ 1.1 TB/年(绝大多数团队的默认答案,💀痛点是 backfill 会静默改写历史分区)、内容寻址 ≈ 3.8 TB/年(大表 + 严格回溯场景,需要一层元数据服务)、DVC 类(本质是内容寻址 + git 存指针,中小团队/实验型项目)。⭐选型先算日变化率:<2% 内容寻址几乎免费;2–20% 分区 + 每月全量快照;>20% 说明口径不稳定,先去问为什么,这不是存储问题。⭐但真相是:绝大多数团队不需要上任何工具,只需要两条纪律——①分区永不覆盖 ②每次训练把用到的分区名和指纹写进 manifest。 🔑指纹解决"它还是不是原来那份":逻辑指纹要按列名排序(免疫 SELECT * 列序变化)、round 浮点(否则两台机器永远对不上)、用 XOR 这类可交换聚合(免疫分布式产出顺序)。⭐⭐一定要存 per_column 而不只存总指纹——总指纹只说"变了",per_column 说"哪一列变了",这是排查从两天缩到十分钟的差别。 四个坑:浮点精度、列顺序、时区(统一 UTC)、行顺序。🧾血缘要记:上游表+分区、每个上游的指纹、SQL 的 commit、参数(⭐随机种子最常漏)、过滤条件(写在 SQL 里但没人会读三个月前的 SQL)、⭐⭐数据截止时间而不是任务运行时间(任务重跑三次运行时间不同、截止时间相同,混淆会直接导致误判)、行数与空值率、触发方式。⭐manifest 要写在产出物旁边(_MANIFEST.json),不要写在另一个系统里——💀血缘系统的存活率比数据文件低得多,它可能换域名、换负责人、或已经下线。 ☠️两种死法A 上游被覆盖(OVERWRITE 或保留期到期,连"当时是什么"都拿不到);💀B 上游还在但语义变了——有人改口径并回填了全部历史分区,分区名没变、行数没变、schema 没变,你重跑结果不同却不知道为什么;⭐没有指纹的话这个问题无法被发现也无法被证明。💀backfill 是数据侧最危险的动作,因为它长得像修 bug:"我们发现算错了,已修好并回填历史"——听起来完全正确,但它同时静默改写了所有历史模型的训练数据。⭐纪律:任何 backfill 必须写新分区或新版本号,永不原地改写。 💀信贷事故:授信模型 v37 训练用 dt=2024-01-31 分区、该表保留期 90 天;3 月有人把 income_verified 从"近 6 个月流水"改成"近 12 个月"并 backfill 全部历史分区;10 月监管要求复现 3 月 12 日的一次拒绝决策,原分区早已被清理。重跑 AUC 0.7412 vs 原始 0.7389(差 0.0023),Top-10 特征重要性 3 个换位,而那位申请人的分数从 0.31 变成 0.44、阈值 0.35——重跑出来的模型会批准他。没被发现的四条:保留期 90 天 < 追溯期 24 个月(这两个数字从没被放在一起看过)、backfill 没产生新分区也没发通知、⭐团队"有"血缘系统但它只记表名不记分区指纹和口径版本(💀有血缘 ≠ 可复现,这条最容易被忽悠过去)、⭐⭐AUC 波动本来就在 0.73–0.75,0.0023 小到不触发任何告警,但足以翻转个体决策——聚合指标的稳定完全掩盖了个体决策的不稳定。代价:6 周专项(2 工程师+1 合规)、咨询法务 ≈86 万、模型下线整改 5 周期间通过率 −11%、少放款 2.3 亿、利息损失 ≈340 万、23 个模型补快照 +41 TB——而💀当初存快照单模型每年约 5000 元,省下的 5000 元最后花掉 400 多万。⭐⭐这案例最值钱的一句:AUC 差 0.0023 是"没差别",同一个人的分数差 0.13 是"完全不同的决策"——可复现性不能用聚合指标验证,必须抽 1000 条个体样本逐条比分,|Δscore| > 0.02 的比例 > 1% 就算复现失败,因为你要向监管解释的从来不是平均值,是某一个人。 🤝和《模型上线之后 17》的分工:那边管代码/参数/环境/模型权重,回答"这个预测是哪个模型版本给的";这边只管数据,回答"这个模型是哪份数据训的"。⚠️边界特别容易出事,因为模型侧觉得数据是数仓的事、数据侧觉得训练集是算法自己拉的,两边都以为对方管了;⭐解法是把「上游分区指纹」列进模型卡必填项,谁发版谁填,填不出来不许发版。🪜分级实施:L0 分区永不覆盖 + 分区名打日志(成本几乎为零);⭐⭐L1 加一个 _MANIFEST.json(分区名+指纹+行数+commit+参数),半天工程量,解决 80% 的"三个月后重现不出来"——这是绝大多数团队应该停下来的地方;L2 正式发版做内容寻址快照 + 保留期对齐追溯期 + 季度复现演练;L3 才上 DVC/lakeFS/Delta 和血缘平台。剩下 20% 才需要工具,而那 20% 的团队自己知道自己是谁。

下一节 👉 18-特征存储.md

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