20 · 实战与挑战项目
⏱ 项目 1–2 天 / 挑战 4–6 天 | ⭐ 这一套必须动手才有用
🎯 一句话
这一套教程的所有内容,都只有在你亲手制造过一次故障之后才会变成直觉。 所以这四个项目的产出不是模型,而是 —— 你亲手造的一次事故,以及你抓住它的全过程。
🧰 开工前:造一个能反复实验的沙盘
import numpy as np, pandas as pd
rng = np.random.default_rng(0)
def make_stream(n_days=120, n_per_day=2000, drift_day=None, break_day=None):
"""生成一条带时间的数据流,可注入漂移和管道故障"""
rows = []
for d in range(n_days):
n = n_per_day
age = rng.normal(35, 10, n)
# ⭐ drift_day 之后,人群整体变年轻(数据漂移)
if drift_day and d >= drift_day:
age -= min((d - drift_day) * 0.3, 10)
income = rng.lognormal(10, 0.6, n)
# ⭐ break_day 那天,income 单位变了(管道故障)
if break_day and d == break_day:
income = income * 1000
# 真实规律
logit = -3 + 0.03 * (age - 35) + 0.4 * np.log(income / 20000)
y = rng.random(n) < 1 / (1 + np.exp(-logit))
rows.append(pd.DataFrame({"day": d, "age": age, "income": income, "y": y}))
return pd.concat(rows, ignore_index=True)
⭐ 有了这个沙盘,你可以在几分钟内造出真实世界要几个月才遇到一次的故障。 四个项目都建立在它上面。
再给一个「训练 + 逐日回放」的骨架,省掉一小时的脚手架工作:
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score
FEATS = ["age", "income"]
def train_and_replay(df, train_days=60):
tr = df[df.day < train_days]
sc = StandardScaler().fit(tr[FEATS]) # ⭐⭐ 只在训练期 fit,之后永远只 transform
m = LogisticRegression().fit(sc.transform(tr[FEATS]), tr.y)
rows = []
for d, g in df[df.day >= train_days].groupby("day"):
p = m.predict_proba(sc.transform(g[FEATS]))[:, 1] # ⭐ 用同一个 sc
rows.append({"day": d,
"AUC": roc_auc_score(g.y, p),
"预测均值": p.mean(), # ⭐ 没有标签也能看的信号
"PSI_income": psi(g.income, tr.income)}) # psi 用第 6 章的实现
return pd.DataFrame(rows)
💀 上面那行
⭐⭐是这一整套项目最容易翻车的地方: 如果你在预测时对每一天的数据重新fit一次 StandardScaler, 注入的管道故障会被归一化自己抹平,你什么都看不到 —— 然后你会以为「监控没用」。 这正是第 4 章那个坑,而它在这里会真实地咬你一口。
🥉 项目一:亲手制造并抓住三种故障(1 天)
目标:让第 1、4、6 章的三类问题在你眼前发生,并用监控抓住。
- [ ] T1 (45min) 用前 60 天训一个模型,在后 60 天上「上线」,画出逐日 AUC
- [ ] T2 (45min) 注入管道故障(
break_day=80,income 单位变了) - 观察:AUC 掉多少?预测值分布怎么变?
- ⭐ 关键问题:如果没有标签,你能发现吗?
- [ ] T3 (45min) 注入数据漂移(
drift_day=70,人群变年轻) - 用 PSI 检测,看它在第几天越过 0.25
- [ ] T4 (60min) 实现第 5 章的
feature_profile+check, 让它自动报出上面两次故障 - [ ] T5 (30min) 对比:监控发现的时间 vs 真实标签发现的时间,差几天?
📏 你该看到的参考数字(差太多说明哪里错了)
| 观测 | 参考范围 | 说明 |
|---|---|---|
| T1 基线逐日 AUC | 0.80 – 0.83 小幅波动 | 看到 0.95+ 说明标签泄漏进特征了,回去查 |
| T2 故障当天 AUC | 0.62 – 0.70 | ⭐ 掉但不归零 —— age 还在起作用 |
| T2 预测分数均值 | 从 ≈0.045 跳到 ≈0.20 | ⭐⭐ 这是不需要标签就能看见的信号 |
T2 income 的 PSI |
> 3(相对训练期) | 极大,一眼可见 |
| T2 第二天 | 自动恢复 | 因为只注入了一天 |
T3 age 的 PSI 越过 0.25 |
漂移开始后 第 12–18 天 | |
| T3 AUC 明显可见地下滑 | 第 25–35 天(掉 0.01–0.02) | |
| T5 提前量 | ≈ 10–20 天 ⭐⭐ | 这个差值就是 PSI 监控买到的东西 |
⚠️ 最容易翻车的四个地方
| 症状 | 原因 |
|---|---|
| AUC 从第一天就是 0.5 | 特征列顺序或 dtype 不对(就是第 4 章那个坑) |
| AUC 高得离谱(> 0.95) | 造数据时的 logit 变量直接混进了特征 |
PSI 算出来是 inf / nan |
分桶时某个桶为空 —— 加平滑 +1e-6 |
| 注入故障那天 AUC 纹丝不动 💀 | 你在预测时重新 fit 了归一化器,故障被自己抹平了 ⭐⭐ |
✅ 通关标准
- 管道故障能在当天被特征监控发现(不需要标签)⭐
- 数据漂移能被 PSI 在指标明显下滑之前发现,提前量 ≥ 10 天
- 能说清「发现延迟」是多少天,并解释为什么
💡 这个项目的价值在 T5:你会亲眼看到「有监控」和「等指标掉下来」差了多少天 —— 那个天数就是监控的全部价值。
🥈 项目二:训练/推理不一致的七种制造法(1 天)
目标:把第 4 章那七种不一致亲手做出来,看它们各自造成多大损失。
- [ ] T1 (60min) 搭一个「离线训练 + 线上推理」的双管道(可以就是两个函数)
- [ ] T2 (90min) 逐个注入七种不一致,每种记录:
- AUC 掉了多少
- 有没有任何报错 ⭐
- 特征分布监控能不能发现
- [ ] T3 (45min) 实现第 4 章的
compare_features,验证它能全抓出来 - [ ] T4 (30min) 做一张表:七种不一致 × 损失大小 × 能否被监控发现
📏 该做出来的那张表(附参考数字)
基线 AUC 取 0.82。你自己跑出来的数会有出入,但「相对大小」和「能否被发现」应该对得上:
| 不一致类型 | 参考 AUC | 会报错吗 | 特征分布监控能发现吗 |
|---|---|---|---|
| 特征顺序错位 ⭐⭐ | 0.82 → 0.50 ± 0.03 | ❌ | ❌ 每列分布都没变,只是对错了位 |
| 类别静默映射到未知 | 0.82 → 0.76 | ❌ | ❌ 分布是真的没变 |
| 归一化参数在推理时重算 | 0.82 → 0.71 | ❌ | ✅ 预测分布明显平移 |
| 缺失值填充策略不同(0 vs 中位数) | 0.82 → 0.78 | ❌ | ⚠️ 只有部分特征看得出 |
| 时间窗口口径不同(近 7 天 vs 近 7 个自然日) | 0.82 → 0.80 | ❌ | ⚠️ 很难 |
| 数值精度(float32 vs float64) | 0.82 → 0.8199 | ❌ | ❌(也几乎不用管) |
| 特征穿越(训练用了未来) 💀 | 离线 0.94 / 线上 0.68 | ❌ | ❌ |
⭐ 这张表的结论:七种里只有两三种能被分布监控抓到。 所以第 4 章的逐字段比对不是"更严谨一点", 它是唯一能覆盖另外四五种的手段。
⚠️ 最容易翻车的地方
💀 「七种我都注入了,但 AUC 都没怎么变」
→ 99% 是因为你的"线上推理"和"离线训练"其实是同一段代码,
中间根本没有真正断开。
⭐ 正确的做法:写成【两个互相不 import 的函数】,
故意让它们只通过一个 dict(模拟 JSON 请求)通信 ——
这样才能造出真实的不一致。
🥇 挑战项目 A:复现辛普森悖论并做对因果(3–4 天)
难度 ★★★★ | 练第 12–14 章
🧰 起步骨架(直接抄)
import numpy as np, pandas as pd
def make_simpson(n=24000, true_lift=0.10, seed=0):
"""⭐⭐ 你自己造数据,所以你知道真值 —— 这就是这个项目的全部价值"""
rng = np.random.default_rng(seed)
is_new = rng.random(n) < 0.5 # 新老用户各一半
# ⭐ 混杂:新用户【更容易】被分到新算法 —— 这就是那个"分流不均"
treat = rng.random(n) < np.where(is_new, 0.83, 0.17)
base = np.where(is_new, 0.020, 0.090) # 天然转化率差 4.5 倍
p = base * (1 + true_lift * treat) # ⭐ 真值:相对提升 10%
y = rng.random(n) < p
return pd.DataFrame({"is_new": is_new, "treat": treat, "y": y})
df = make_simpson()
print(df.groupby(["is_new", "treat"]).y.mean()) # 分人群:新算法都赢
print(df.groupby("treat").y.mean()) # 总体:反过来 💀
阶段一:造出悖论
- [ ] T1 构造一个数据生成过程,使得新策略在每个子群都更好、总体更差
- [ ] T2 复现第 14 章那张表的数字(9.50/9.00、2.20/2.00、3.42/7.83)
- [ ] T3 用 SRM 检查,验证它能不能预警这种情况
阶段二:用四种方法估计真实效应
- [ ] T4 已知真实效应(你自己造的数据,所以知道真值)⭐
- [ ] T5 分别用:朴素比较 / 分层加权 / PSM / DID,各估一次
- [ ] T6 对比每种方法的估计值和真值的偏差
阶段三:破坏假设,看方法什么时候失效
- [ ] T7 加一个未观测的混杂变量(不放进 PSM 的协变量里)
- 看 PSM 的估计偏差变多大 ⭐
- [ ] T8 破坏 DID 的平行趋势假设,看它错多少
- [ ] T9 做敏感性分析:多强的未观测混杂能推翻结论?
✅ 通关标准(带数字,可以自己判卷)
| 判据 | 参考值 |
|---|---|
| 悖论成功复现 | 分人群两组都是新算法赢,总体反过来 |
| 朴素比较的相对偏差 | > 100%(连符号都是错的)💀 |
| 分层加权的相对偏差 | < 3% ✅ |
PSM(协变量含 is_new)的相对偏差 |
< 5% ✅ |
PSM + 未观测混杂(is_new 不进协变量) |
偏差涨到 > 20% ⭐⭐ 这条是核心 |
| 报告 | 每种方法依赖什么假设,写清楚 |
⚠️ 最容易翻车的地方
💀 「PSM 加了未观测混杂,偏差却没怎么变」
→ 你的未观测混杂强度不够。
它必须【同时强烈影响 treat 和 y】才会造成偏差 ——
只影响其中一个是不会的 ⭐
💀 「分层加权算出来和朴素比较一样」
→ 你八成是用【各组自己的样本量】做权重了。
必须用【同一套权重】(通常是总体的人群构成),
这一点第 14 章讲过。
💀 「样本量 24000 时结果每次都不一样」
→ 正常。⭐ 把 seed 循环 200 次,报【偏差的均值和分布】,
而不是单次结果 —— 这才是"系统性偏差"的正确证明方式
🔑 这个项目的核心价值: 你是唯一知道真值的人 —— 这在真实工作里永远做不到。 正是这一点让你能真正看清每种方法在什么时候会骗你。
🏆 挑战项目 B:搭一个完整的模型运维沙盘(4–6 天)
难度 ★★★★★ | 把整套教程串起来
目标:一个能【自动发现问题、定位原因、决定是否重训】的闭环
- [ ] T1 数据流 + 模型服务 + 每日重训任务(用沙盘生成)
- [ ] T2 三层监控(第 5 章)+ 告警分级(第 8 章)
- ⭐ 用历史数据回测告警阈值,把误报率压到可接受
- [ ] T3 随机注入故障:管道坏 / 数据漂移 / 概念漂移 / 特征不一致(随机选、随机时间)
- [ ] T4 实现第 11 章的排查流程,尽量自动化:
- 自动定位拐点 → 自动拆人群 → 自动做 SHAP 对比
- [ ] T5 实现第 16 章的五道闸门,验证它能挡住「管道坏了还硬训」 ⭐
- [ ] T6 记录每次故障的发现延迟和定位延迟
- [ ] T7 迭代三轮,让这两个数字下降
✅ 通关标准(带数字)
| 判据 | 参考值 |
|---|---|
| 自动区分四种故障类型 ⭐⭐ | 随机注入 10 次,至少分对 8 次(不只是报"有问题") |
| 闸门拦截 | 至少挡住 1 次「本不该重训的重训」,并留下拦截理由 |
| 发现延迟 | 三轮迭代后平均下降 ≥ 50%,且绝对值 < 6 小时 |
| 误报率 ⭐ | 连续 30 天无故障期内,P1 告警 ≤ 2 次 |
| 定位延迟 | 从告警到定位到根因 < 30 分钟(自动化部分) |
| 文档 | 一份完整的模型卡(第 19 章) |
⭐ 「误报率」那一行是最容易被跳过、也最能拉开差距的一条: 一个把所有阈值都调到极敏感的系统,发现延迟会非常好看 —— 然后它每天报 20 次警,三周后就没人看了(第 8 章)。 这两个指标必须一起报,只报一个等于没做。
⚠️ 最容易翻车的地方
💀 「四种故障我的系统全部报成'数据漂移'」
→ 你只用了 PSI 一个信号。
⭐ 区分四种至少需要四路信号:
· 特征分布突变(PSI 一天内跳) → 管道
· 特征分布缓慢移动 → 数据漂移
· 特征分布没变、但 预测↔标签 关系变了 → 概念漂移
· 训练/推理逐字段比对不通过 → 一致性
⭐⭐ 关键在【突变 vs 缓变】和【输入变了 vs 关系变了】这两个轴
💀 「回测阈值时误报率永远压不下来」
→ 多半是你在用【绝对阈值】。
改成【相对于同一星期几、同一时段的基线】,误报会大幅下降
💀 「三轮迭代后发现延迟没降」
→ 检查你每轮到底改了什么。⭐ 有效的改动通常是
"把某个信号从业务层往输入层前移",而不是"把阈值调低"
🏆 加分
- 加入反馈闭环(模型影响下一批数据),观察不做纠偏时的退化
- 实现长期 holdback 组,对比累计效果(第 15 章)
- 实现第 17 章的黄金样本自检,并故意升级一次依赖验证它能拦住
📝 报告模板
# 事故 / 实验:____
## 注入的问题(真值)
## 监控发出的第一个信号 & 时间
## 排查路径(按第 11 章六步)
## 定位到的根因 vs 真值 —— 对上了吗?
## 发现延迟 / 定位延迟
## 改进项:加什么能更早发现 ← 最有价值的一节
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 推荐算法 20 | 同类沙盘:也是自己造环境,观察模型把环境带偏 |
| Kaggle 25 | 沙盘的骨架可以从那份 Pipeline 模板起步 |
| 智能体 19 | ⭐ 「先建评测,再写实现」的同一条纪律,那边有完整的七天切分 |
✅ 检查点
- 为什么这套教程必须动手?沙盘解决了什么问题?
- 项目一里最有价值的是哪一步?参考的提前量大概是多少天?
- 项目一里「注入故障那天 AUC 纹丝不动」最可能是什么原因?
- 项目二那张表要回答什么问题?七种不一致里,分布监控大概能抓到几种?说明了什么?
- 项目二里「七种都注入了但 AUC 都没变」通常是什么原因?
- 挑战 A 的核心价值是什么?为什么真实工作里做不到?
- 挑战 A 里,PSM 加了未观测混杂后偏差却没变,最可能是什么原因?
- 挑战 B 为什么必须同时报「发现延迟」和「误报率」?
- 要自动区分四种故障类型,至少需要哪几路信号?关键的两个判别轴是什么?
👀 答案
- 因为监控、排查、归因这些能力,只有在亲手制造过一次故障之后才会变成直觉。沙盘让你在几分钟内造出真实世界要几个月才遇到一次的故障。
- T5:对比「监控发现的时间」和「真实标签发现的时间」。参考数字:
age的 PSI 在漂移开始后第 12–18 天越过 0.25,而 AUC 要到第 25–35 天才明显下滑 —— 提前量约 10–20 天,那个差值就是监控买到的东西。 - 你在预测时重新
fit了归一化器,注入的故障被归一化自己抹平了。归一化参数必须只在训练期 fit,之后永远只 transform——这正是第 4 章那个坑,在这里会真实地咬你一口。 - 七种不一致 × 损失大小 × 能否被监控发现。分布监控只能抓到两三种(归一化参数重算、部分缺失值填充差异);特征顺序错位(0.82→0.50)和类别静默映射(0.82→0.76)它都发现不了,因为每一列的分布都真的没变。说明逐字段比对不是"更严谨一点",它是唯一能覆盖另外四五种的手段。
- 因为你的"线上推理"和"离线训练"其实是同一段代码,中间根本没断开。正确做法是写成两个互相不 import 的函数,只通过一个 dict(模拟 JSON 请求)通信。
- 你是唯一知道真值的人。真实工作里永远做不到——正是这一点让你能看清每种方法在什么时候会骗你。
- 未观测混杂的强度不够。它必须同时强烈影响
treat和y才会造成偏差,只影响其中一个是不会的。另外单次结果波动很大,应该循环 200 个 seed,报偏差的均值和分布——这才是"系统性偏差"的正确证明方式。 - 因为只报发现延迟的话,把所有阈值调到极敏感就能拿满分 —— 然后系统每天报 20 次警,三周后就没人看了(第 8 章)。两个指标必须一起报,只报一个等于没做。
- 至少四路:特征分布一天内突变(管道)、特征分布缓慢移动(数据漂移)、特征分布没变但预测↔标签关系变了(概念漂移)、训练/推理逐字段比对不通过(一致性)。两个判别轴是 「突变 vs 缓变」 和 「输入变了 vs 关系变了」。
🛑 可以停在这里
⚡ 走神救援
⭐这套教程必须动手——监控/排查/归因只有亲手造过一次故障才会变成直觉。沙盘(可注入漂移和管道故障的数据生成器)让你几分钟造出真实世界几个月才遇一次的故障。项目一:注入管道故障和数据漂移,用特征监控抓,⭐最有价值的是 T5——对比「监控发现的时间」和「等标签掉下来」差几天,那个天数就是监控的全部价值。项目二:亲手做出七种训练/推理不一致,做一张七种×损失×能否被监控发现的表 —— ⭐特征顺序错位 AUC 掉到接近0.5 但零报错、类别静默映射连分布监控都发现不了。⭐挑战A:复现辛普森悖论 + 用四种方法估真实效应,核心价值是你是唯一知道真值的人(真实工作永远做不到),所以能看清每种方法什么时候会骗你;阶段三故意破坏假设(给PSM加未观测混杂、破坏DID平行趋势)。挑战B:完整运维沙盘,最难的通关标准是⭐⭐系统能自动区分四种故障类型而不只是报"有问题"(随机注入10次至少分对8次),还要验证五道闸门能挡住「管道坏了还硬训」,并让发现延迟三轮迭代下降50%且绝对值小于6小时。⭐参考数字(对不上就是哪里错了):基线逐日 AUC 0.80–0.83(看到 0.95+ 是标签泄漏);管道故障当天 AUC 掉到 0.62–0.70、预测均值从 0.045 跳到 0.20(⭐⭐这是不需要标签就能看见的信号);数据漂移时 PSI 在第 12–18 天越线、AUC 到第 25–35 天才明显下滑 → 提前量 10–20 天。💀最容易翻车的四件事:①⭐⭐预测时重新 fit 归一化器 → 注入的故障被自己抹平了,然后你以为"监控没用"(这就是第4章的坑)②七种不一致都注入了却 AUC 不变 → 你的"线上"和"离线"其实是同一段代码,要写成两个互不 import、只通过 dict 通信的函数 ③PSM 加了未观测混杂偏差却没变 → 混杂必须同时强烈影响 treat 和 y,而且要循环 200 个 seed 报均值和分布才叫证明了系统性偏差 ④分层加权算出来和朴素比较一样 → 你用了各组自己的样本量当权重,必须用同一套权重。⭐项目二那张表的真正结论:七种不一致里分布监控只抓得到两三种(特征顺序错位 0.82→0.50、类别静默映射 0.82→0.76 它都发现不了,因为每列分布真的没变)→ 逐字段比对不是"更严谨一点",是唯一能覆盖其余的手段。⭐挑战B 必须同时报「发现延迟」和「误报率」(无故障期 30 天内 P1 告警 ≤ 2 次)——只报延迟的话,把阈值调到极敏感就能拿满分,然后三周后没人看告警了;要自动区分四种故障,关键是「突变 vs 缓变」和「输入变了 vs 关系变了」这两个判别轴。
下一节 👉 附录A-速查.md