第21章 稳定性与偏置审计(数字会骗人,切片会说话)
小白一句话
前几章的评估都是"一个时间点、一个整体数字"。这一章把同一套评估放到多个时间点和多个子人群上重看:指标会不会漂,某些人是不是被系统性地高估或低估。
这一章在干嘛
整体 AUC 0.70、Brier 0.18 看起来还行,但这只是"某一天、全体人"的一个快照。本章做三种审计:
- 滚动起点:换个训练截止日、往后多测几个时间点,看指标是稳还是漂。
- 配对显著性:两个模型(或两个配置)差的那点数字,统计上是不是真的分得出来。
- 偏置切片:把人群切开,看"谁被高估、谁被低估"------整体好看不代表处处公平。
这三件事串起来就是本章的立场:整体数字好 ≠ 处处都好;单点数字 ≠ 稳定。前两章说"别信单点小样本数字",本章说"还要别信整体数字"。
1) 滚动起点:指标会漂吗
示例数据只有 3 个评分日,滚动评估能做两个点:
| 评估点 | 训练 | 测试 | 测试基准率 | AUC | Brier |
|---|---|---|---|---|---|
| 点 1 | 07-08 | 07-15(56 行,49 正) | 0.875 | 0.695 | 0.110 |
| 点 2 | 07-08 + 07-15 | 07-22(57 行,42 正) | 0.737 | 0.698 | 0.178 |
AUC 几乎没动(0.695 → 0.698)------排序质量很稳 。但 Brier 从 0.110 涨到 0.178------概率刻度明显变差。为什么?
点 2 的测试集更难:基准率从 0.875 掉到 0.737,里面开始出现 U0009、U0025 这种"之前活跃、正在掉"的人(第16、17章的熟人)。人还在活跃的时候谁都能猜准,Brier 当然小;等有人开始凉了,校准就跟不上了。
结论:AUC 稳不代表 Brier 稳。不同指标对"任务变难"的敏感度不一样。正式上线前要多看几个滚动点,别拿单点数字当长期表现------这也是第29章"上线后监控再训练"要做的事的预演。
滚动前推(walk-forward):把"多看几个点"做成正式协议
上面只有 07-15、07-22 两个评估点,是快检。要更稳的评估,用滚动前推验证(walk-forward CV):把评分日排成一串,每个评估点用"它之前所有评分日的成熟样本"训练,评"下一个评分日"------训练窗口随评估点逐步扩展,测试永远在训练之后,绝不回头。
示例数据只有 30 天,取 8 个隔天评分日(07-08、07-10、...、07-22),凑出 7 个评估点(附件/06_评估篇/activity_walkforward.py):
| 评估点 | 训练评分日 | 训练行数 | 测试行数 | 基准率 | PR-AUC | AUC | Brier |
|---|---|---|---|---|---|---|---|
| 评 07-10 | 1 | 49 | 54 | 0.870 | 0.955 | 0.719 | 0.174 |
| 评 07-12 | 2 | 103 | 55 | 0.855 | 0.950 | 0.816 | 0.120 |
| 评 07-14 | 3 | 158 | 56 | 0.875 | 0.987 | 0.914 | 0.097 |
| 评 07-16 | 4 | 214 | 57 | 0.860 | 0.944 | 0.821 | 0.102 |
| 评 07-18 | 5 | 271 | 57 | 0.807 | 0.984 | 0.927 | 0.109 |
| 评 07-20 | 6 | 328 | 57 | 0.807 | 0.939 | 0.805 | 0.137 |
| 评 07-22 | 7 | 385 | 57 | 0.737 | 0.948 | 0.863 | 0.133 |
这 7 个点给出两条单点评估给不了的信息:
- 训练评分日攒得越多,指标越稳 。评 07-22 是同一个测试集:只用 2 个评分日训(第 21 章滚动起点)AUC 0.698,用 7 个评分日训(walk-forward 末点)0.863。训练窗口每扩展一步,AUC 大体爬一截(0.719 → 0.816 → 0.914 → ...)。这就是第 8 章"多评分日样本"和第 29 章"重训前把历史攒够"的评估侧证据------样本量这道线,评估阶段同样看得见。
- 7 个点自己也在晃 。AUC 0.72~0.93、Brier 0.097~0.174,均值 ± 一个标准差是 0.124 ± 0.024。单看任何一个点都会高估或低估模型:点 1(训 49 行)0.719 偏悲观,点 5(训 271 行)0.927 偏乐观。上线前对指标的预期,应该用这个范围而不是某一次的数------第 29 章监控报警的参照线,就是这么来的。
两点代价要认:一是多个滚动点意味着多训好几遍模型(每点一次全流程),评估成本上去了;二是评分日之间隔太近,相邻点的样本高度重复(同一个人的 07-10 和 07-12 特征几乎一样),信息增量递减------真实项目按周或按月滚动,比这个示例的隔天更合适。
2) 配对显著性:差异是真的吗
拿同一个测试集(07-22 的 57 人),比两个模型:
- 全特征模型:前几章一直用的主角------10 个特征全用,第12~20章那个 AUC 0.698、Brier 0.178 就是它。
- 去掉趋势特征的模型 :本章现造的消融变体------同一个 GBDT,只是输入里少掉
f_trend一列,其余 9 列、超参、切分、校准、种子全部一样,前文没有这个模型。造它就是为了看"少一个特征,这点差别在统计上分不分得出"。
结果:
- AUC:全特征 0.698 vs 去趋势 0.728(去趋势反而略高?)
- Brier:0.178 vs 0.177(几乎一样)
- 两模型判得不一样的人只有 3 个:全特征对而另一错 2 个,去趋势对而全特征错 1 个
- McNemar 检验 p = 1.000
p=1.000 的意思是:在 57 个样本上,这点差异统计上完全分不出来。而且去趋势的 AUC 还略高,更说明这不是"趋势特征没用"的证据,就是小样本噪声------第12章明明说 f_trend 有 0.084 的重要性、能帮 U0009 加分,但在这 57 人身上根本体现不出来。
McNemar 检验把两个模型放到一张小表里看,逻辑是这样的:
- 把 57 个样本按两个模型的判断分成四格:都对、都错、全特征对而另一错(b)、另一对而全特征错(c)。前两格两边表现一样,不提供"谁更好"的信息,只有 b 和 c 有用。
- 如果两个模型其实一样好,b 和 c 应该大致相当(谁对谁错只是运气);如果 b 远大于 c,说明全特征模型真的更会判,反过来同理。
- 检验就是算"b 和 c 偏成这样、靠运气发生的概率"------这个概率就是 p 值,通常 p<0.05 才算显著差异。
本例 b=2、c=1,几乎对称,p=1.000------运气完全能解释,分不出谁好。再补一句它的局限:McNemar 只看 0/1 判断的对错,看不见排序上的细微差异(比如 AUC 0.698 vs 0.728 这种);所以"没有显著差异"不等于"两个模型一模一样",只是"这点样本上分不出"。
结论:小样本上别拿微小差异当结论。真要比较两个配置,要么攒更多样本、多跑几个滚动点,要么上线做 A/B(第29章会提到)。这一节和"换种子就抖"(第20章)是一对兄弟:一个是数字会跳,一个是跳了你还分不出谁好。
换个招:同窗配对 + bootstrap,给"差多少"配个区间
McNemar 有个够不着的地方:它只数"判对/判错",AUC 0.698 和 0.728 这种排序上的细微差别它看不见。想比较排序指标,更常用的是 bootstrap:把评估样本反复有放回地抽,每次都重算两个模型的差值,抽上几千次,就能看出"这个差距大概在什么范围里晃"。
两个模型放在同一份评估样本上比(这叫同窗配对),比的是模型本身,不掺数据的差别。
具体做一遍。评估样本用第19章那套------把评分日撑密到 07-16 ~ 07-22,57 个人一共 399 行。两个模型还是上面那对(全特征 vs 去趋势),比的是 PR-AUC 的差值 Δ:
- 全特征 PR-AUC 0.907
- 去趋势 PR-AUC 0.923
- Δ = 全特征 − 去趋势 = −0.016(这回是去趋势略好)
光看这一个数没意义,bootstrap 抽 2000 次看 Δ 的分布:
| 抽法 | Δ 均值 | 95% 置信区间 | 区间宽度 | P(Δ>0) |
|---|---|---|---|---|
| UID 精确抽样(按用户抽) | −0.015 | −0.052, +0.018 | 0.071 | 0.197 |
| 按行抽样(把每行当独立样本) | −0.015 | −0.041, +0.010 | 0.051 | 0.139 |
怎么读这两行:
- 置信区间跨过了 0 (−0.052 到 +0.018),意思是"这个差距和 0 分不开"------Δ 有时是正的、有时是负的,全看抽到哪拨人。换成人话:两个模型没分出高下。
- P(Δ>0) = 0.197,只有不到两成的抽样里全特征占优。要下"全特征更好"的结论,这个数通常得奔着 0.95 去。所以不支持。
- 这个结论和上面 McNemar 的 p=1.000 对上了 ------两招路子不同,都指向同一件事:这份数据上,两个模型分不出谁好。 交叉验证过一遍,心里就踏实了。
抽样的单位很关键。 表里第二种抽法(按行抽)给出的区间比按用户抽窄了 28% 。看着更"精确",其实是假的:同一用户的 7 天被当成 7 个独立样本,等于凭空多出来许多"新信息",不确定性被系统性低估了。这和第19章那个"快照级指标虚高"是同一件事------抽样单位要和评估口径一致,评估按人(UID 级)就按人抽,别把一个人的多次快照拆开当多个人用。
再补一句 bootstrap 的边界:它估的是"这份评估样本上的不确定性",估不出"换一批人、换一段时间还一不一样"。后者得靠滚动起点(前面第 1 节)和换种子(第20章)来看。三种招数凑一起,才敢说一个结论稳不稳。
3) 偏置切片:谁被高估了
整体 Brier 0.178 好看,不代表每个子人群都被公平对待。按活跃度、新近度把测试集切开,看"模型平均预测 vs 实际回访率":
| 切片 | 人数 | 实际回访率 | 平均预测 | 偏差(正=高估) |
|---|---|---|---|---|
| 高活跃(总领取 > 3.5) | 43 | 0.860 | 0.917 | +0.056 |
| 低活跃(总领取 ≤ 3.5) | 14 | 0.357 | 0.588 | +0.231 |
| 久没来(距上次 ≥ 4 天) | 15 | 0.467 | 0.629 | +0.163 |
| 近期还来(距上次 ≤ 2 天) | 37 | 0.838 | 0.914 | +0.076 |
审计结论很扎眼:模型系统性高估"低活跃、久没来"的人 ------低活跃那 14 人实际只有 36% 回来,模型平均给到 59%,高估了两成多。这不是随机抖动:它和第16章 U0009"满格误判"是同一件事在人群层面上的表现------对"看起来要凉的人",模型过分乐观。
业务上的含义:如果运营拿"模型说 0.6"当成"六成会回",去决定一个久没来的人"不用管",会系统性错判。审计结果告诉我们要么对这类人单独设更低的阈值、更保守的分档,要么继续补特征(第12章说过,能抓 U0009 的是趋势这类更灵敏的特征,而不是校准)。
两个读表的坑,免得误读:
- 片内 AUC 别当回事。高活跃片内几乎全是正样本(43 人 86% 都回),没得排序,AUC 只有 0.42------不是"模型在这片里差",是"片内太同质,AUC 失去意义"。切片审计主要看"预测 vs 实际"的偏差,不是片内 AUC。
- 切片越小越抖 。14~15 人的片,偏差 0.16~0.23 这个方向可信,具体小数别抠。
它和前后章节的关系
- 第19章讲指标、第20章讲两种评估视角,本章把"时间"(滚动起点)和"人群"(偏置切片)两个维度再切一刀。
- 第29章上线后的监控再训练,就是滚动起点评估的自动化版本;偏置审计发现的问题(低活跃被高估),决定了上线前要不要调阈值、补特征。
动手
- 跑
附件/06_评估篇/activity_audit.py,对照上面三块输出:滚动表、McNemar、切片表。 - 把第 2 部分的"去趋势"改成"去新近度特征"(把脚本里
FEATS_NO_TREND换成去掉f_recency的那份)再跑,看这次能不能分出差异------新近度可能是比趋势更"大"的特征,样本小也可能更明显。 - 跑
附件/06_评估篇/activity_bootstrap.py,看那张 bootstrap 表:Δ 的 95% 置信区间是不是跨过 0、P(Δ>0) 离 0.95 还差多远;再把按行抽样和 UID 精确抽样两个区间宽度对比一下,看前者窄了多少。 - 把
activity_bootstrap.py里的boot_by_uid换成按行抽(或者反过来把行数改成只取 07-22 一天),看置信区间怎么变------评估样本越小、每人票数越少,区间就越宽,这是"样本量决定你能下多细的结论"。 - 低活跃人群被高估 0.23,运营怎么接?别用全局阈值;对低活跃/久没来的人单独设低阈值或用更保守的分档,或继续补特征。
- 如果滚动评估发现 Brier 一路恶化、AUC 也在掉,先查三样:人群结构是不是变了、特征口径是不是漂了、标签定义是不是改了------这三样变了,模型没变也是废的。
- 跑
附件/06_评估篇/activity_walkforward.py,对照上面那张 7 点表。把评分日间隔从 2 天改成 1 天(AS_OF_LIST那行的2 * k改成k),重跑看评估点数变多少、相邻点数字是不是更接近------评分日越密,重复信息越多。 - 从 walk-forward 的 7 个点里只看最后一个(评 07-22)当"模型表现",再和均值 ± 一个标准差比------想想上线前报哪个数、监控阈值拿什么当参照线。
本章配套脚本
附件/06_评估篇/activity_audit.py(滚动起点评估 + 全特征 vs 去趋势的 McNemar 配对检验 + 按活跃度/新近度的偏置切片)、附件/06_评估篇/activity_bootstrap.py(同窗配对 + UID 精确 bootstrap:ΔPR-AUC 的 95% 置信区间、P(Δ>0)、按行抽样与按用户抽样的对比、换种子复现)、附件/06_评估篇/activity_walkforward.py(8 个隔天评分日、7 个扩展窗口评估点的 walk-forward 全协议 + 指标轨迹汇总)。用到的 numpy、scikit-learn、scipy 已在第4章附件/00_公共/requirements.txt列出,环境搭建见第4章。训练表由附件/05_活跃度预测篇/activity_train_table.py生成,口径一致。