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

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 基线逐日 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 了归一化器,故障被自己抹平了 ⭐⭐

✅ 通关标准

💡 这个项目的价值在 T5你会亲眼看到「有监控」和「等指标掉下来」差了多少天 —— 那个天数就是监控的全部价值。


🥈 项目二:训练/推理不一致的七种制造法(1 天)

目标:把第 4 章那七种不一致亲手做出来,看它们各自造成多大损失。

📏 该做出来的那张表(附参考数字)

基线 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())               # 总体:反过来 💀

阶段一:造出悖论

阶段二:用四种方法估计真实效应

阶段三:破坏假设,看方法什么时候失效

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

判据 参考值
悖论成功复现 分人群两组都是新算法赢,总体反过来
朴素比较的相对偏差 > 100%(连符号都是错的)💀
分层加权的相对偏差 < 3%
PSM(协变量含 is_new)的相对偏差 < 5%
PSM + 未观测混杂is_new 不进协变量) 偏差涨到 > 20% ⭐⭐ 这条是核心
报告 每种方法依赖什么假设,写清楚

⚠️ 最容易翻车的地方

   💀 「PSM 加了未观测混杂,偏差却没怎么变」
      → 你的未观测混杂强度不够。
        它必须【同时强烈影响 treat 和 y】才会造成偏差 —— 
        只影响其中一个是不会的  ⭐

   💀 「分层加权算出来和朴素比较一样」
      → 你八成是用【各组自己的样本量】做权重了。
        必须用【同一套权重】(通常是总体的人群构成),
        这一点第 14 章讲过。

   💀 「样本量 24000 时结果每次都不一样」
      → 正常。⭐ 把 seed 循环 200 次,报【偏差的均值和分布】,
        而不是单次结果 —— 这才是"系统性偏差"的正确证明方式

🔑 这个项目的核心价值你是唯一知道真值的人 —— 这在真实工作里永远做不到。 正是这一点让你能真正看清每种方法在什么时候会骗你。


🏆 挑战项目 B:搭一个完整的模型运维沙盘(4–6 天)

难度 ★★★★★ | 把整套教程串起来

   目标:一个能【自动发现问题、定位原因、决定是否重训】的闭环

✅ 通关标准(带数字)

判据 参考值
自动区分四种故障类型 ⭐⭐ 随机注入 10 次,至少分对 8 次(不只是报"有问题")
闸门拦截 至少挡住 1 次「本不该重训的重训」,并留下拦截理由
发现延迟 三轮迭代后平均下降 ≥ 50%,且绝对值 < 6 小时
误报率 连续 30 天无故障期内,P1 告警 ≤ 2 次
定位延迟 从告警到定位到根因 < 30 分钟(自动化部分)
文档 一份完整的模型卡(第 19 章

「误报率」那一行是最容易被跳过、也最能拉开差距的一条: 一个把所有阈值都调到极敏感的系统,发现延迟会非常好看 —— 然后它每天报 20 次警,三周后就没人看了第 8 章)。 这两个指标必须一起报,只报一个等于没做。

⚠️ 最容易翻车的地方

   💀 「四种故障我的系统全部报成'数据漂移'」
      → 你只用了 PSI 一个信号。
        ⭐ 区分四种至少需要四路信号:
          · 特征分布突变(PSI 一天内跳)        → 管道
          · 特征分布缓慢移动                    → 数据漂移
          · 特征分布没变、但 预测↔标签 关系变了 → 概念漂移
          · 训练/推理逐字段比对不通过           → 一致性
        ⭐⭐ 关键在【突变 vs 缓变】和【输入变了 vs 关系变了】这两个轴

   💀 「回测阈值时误报率永远压不下来」
      → 多半是你在用【绝对阈值】。
        改成【相对于同一星期几、同一时段的基线】,误报会大幅下降

   💀 「三轮迭代后发现延迟没降」
      → 检查你每轮到底改了什么。⭐ 有效的改动通常是
        "把某个信号从业务层往输入层前移",而不是"把阈值调低"

🏆 加分


📝 报告模板

# 事故 / 实验:____
## 注入的问题(真值)
## 监控发出的第一个信号 & 时间
## 排查路径(按第 11 章六步)
## 定位到的根因 vs 真值 —— 对上了吗?
## 发现延迟 / 定位延迟
## 改进项:加什么能更早发现  ← 最有价值的一节

🔗 这一章连到哪里

去哪 为什么
推荐算法 20 同类沙盘:也是自己造环境,观察模型把环境带偏
Kaggle 25 沙盘的骨架可以从那份 Pipeline 模板起步
智能体 19 ⭐ 「先建评测,再写实现」的同一条纪律,那边有完整的七天切分

✅ 检查点

  1. 为什么这套教程必须动手?沙盘解决了什么问题?
  2. 项目一里最有价值的是哪一步?参考的提前量大概是多少天?
  3. 项目一里「注入故障那天 AUC 纹丝不动」最可能是什么原因?
  4. 项目二那张表要回答什么问题?七种不一致里,分布监控大概能抓到几种?说明了什么?
  5. 项目二里「七种都注入了但 AUC 都没变」通常是什么原因?
  6. 挑战 A 的核心价值是什么?为什么真实工作里做不到?
  7. 挑战 A 里,PSM 加了未观测混杂后偏差却没变,最可能是什么原因?
  8. 挑战 B 为什么必须同时报「发现延迟」和「误报率」?
  9. 要自动区分四种故障类型,至少需要哪几路信号?关键的两个判别轴是什么?
👀 答案
  1. 因为监控、排查、归因这些能力,只有在亲手制造过一次故障之后才会变成直觉。沙盘让你在几分钟内造出真实世界要几个月才遇到一次的故障
  2. T5:对比「监控发现的时间」和「真实标签发现的时间」。参考数字:age 的 PSI 在漂移开始后第 12–18 天越过 0.25,而 AUC 要到第 25–35 天才明显下滑 —— 提前量约 10–20 天,那个差值就是监控买到的东西。
  3. 你在预测时重新 fit 了归一化器,注入的故障被归一化自己抹平了。归一化参数必须只在训练期 fit,之后永远只 transform——这正是第 4 章那个坑,在这里会真实地咬你一口。
  4. 七种不一致 × 损失大小 × 能否被监控发现。分布监控只能抓到两三种(归一化参数重算、部分缺失值填充差异);特征顺序错位(0.82→0.50)和类别静默映射(0.82→0.76)它都发现不了,因为每一列的分布都真的没变。说明逐字段比对不是"更严谨一点",它是唯一能覆盖另外四五种的手段
  5. 因为你的"线上推理"和"离线训练"其实是同一段代码,中间根本没断开。正确做法是写成两个互相不 import 的函数,只通过一个 dict(模拟 JSON 请求)通信。
  6. 你是唯一知道真值的人。真实工作里永远做不到——正是这一点让你能看清每种方法在什么时候会骗你。
  7. 未观测混杂的强度不够。它必须同时强烈影响 treaty 才会造成偏差,只影响其中一个是不会的。另外单次结果波动很大,应该循环 200 个 seed,报偏差的均值和分布——这才是"系统性偏差"的正确证明方式。
  8. 因为只报发现延迟的话,把所有阈值调到极敏感就能拿满分 —— 然后系统每天报 20 次警,三周后就没人看了(第 8 章)。两个指标必须一起报,只报一个等于没做。
  9. 至少四路:特征分布一天内突变(管道)、特征分布缓慢移动(数据漂移)、特征分布没变但预测↔标签关系变了(概念漂移)、训练/推理逐字段比对不通过(一致性)。两个判别轴是 「突变 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

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