小白一句话
模型吐出来的 0.9 不一定真有九成把握。校准就是把"模型嘴上的概率"掰成"真实会发生的比例",让 80 分真的意味着约八成的人会回来。
这一章要补的洞
第12章说模型学的是 P(未来 7 天回来) 这个连续概率;第14章说模型在最小化对数损失,而对数损失会"鼓励"它输出真概率。但"鼓励"不等于"已经做到"。
GBDT 这类树模型实际输出常有偏:树在边界硬切,容易把概率推到极端(动不动就给 0.99 或 0.01),产生"过自信"。所以光靠损失函数还不够,得显式做一步校准------把模型的原始输出重新映射成可信的真概率。第12章提过"乘 100 当 0--100 分",这一步就是让那个分真能当真概率用。
校准在干什么(先建立直觉)
输入是模型原始输出 p_raw,输出是校准后 p_cal。它做的事很克制:
- 单调(保序):原来 A 比 B 概率高,校准后还是 A 高。所以校准改的是"刻度",不是"排序"------谁排前面不变。这点后面看 AUC 时会印证。
- 目标 :让"所有被模型说成 0.7 的人,真实回来比例也约 0.7"。画出来,就是可靠性曲线贴着对角线
y = x。
一句话:校准把"模型以为的把握"修成"实际发生的频率"。
怎么判断需不需要校准
两个看图看数的办法:
- 可靠性曲线(reliability diagram):横轴模型预测概率,纵轴真实发生比例。理想情况是所有点贴着对角线。
- 两个指标 :
- Brier score:每个样本"预测概率"和"真实 0/1"的均方误差,越小越准(理想是 0)。
- ECE(期望校准误差) :把样本按预测概率分桶,每桶算
|该桶平均预测 − 该桶真实比例|,按桶大小加权平均。越小越准。
基于示例数据实测
按第8章的时间外推思路切分,和第15章同一套:训练集 = 07-08、07-15 的 105 行(92 正) ,测试集 = 07-22 的 57 行(42 正),训练用早的、测试用晚的,细节不再重复。在训练集上拟合 GBDT,在测试集上对比"原始输出"和"校准后输出"。
两种校准器,以及小样本的坑
- sigmoid(Platt scaling) :假设"模型得分越高、真实回来的几率越高"这条关系是一条固定的 S 形曲线,于是只学两个参数(S 形的位置和倾斜度),把整体概率掰一次。它只能调"整体偏移",不够灵活,但形状简单、样本少也稳。
- isotonic(保序回归) :不假设任何函数形状,只守一条规矩------"预测高的,校准后还是高"(保序),然后让曲线爱往哪弯往哪弯地贴合真实比例。能弯、更灵活,但代价是每个台阶都得有足够样本撑着。本例测试集才 57 行,isotonic 在 0.64 那个桶给出"预测 0.64 → 实际 0.00"(差 −0.64)------那个桶样本极少,校准被它带偏,是过拟合。
一个只能整体挪、一个能自由弯,代价不同。本例只有 57 行测试样本,isotonic 那种"每个台阶都要样本撑"的性格容易吃亏,先猜 sigmoid 更稳------下面这张表会给数字印证。等样本攒到成千上万行,isotonic 能弯的优势才发挥得出来。
三种输出在测试集上的指标
| 输出 | Brier(越小越准) | ECE(越小越准) | ROC-AUC |
|---|---|---|---|
| 未校准 | 0.224 | 0.230 | 0.689 |
| sigmoid 校准 | 0.178 | 0.122 | 0.698 |
| isotonic 校准 | 0.180 | 0.151 | 0.740 |
校准之后 Brier、ECE 都明显下降------概率变可信了。ROC-AUC 三者都在 0.69~0.74 同一区间,没有本质变化,正说明校准改刻度不改排序(测试集只有 57 行,这点上下浮动是样本小的正常噪声,不是校准把人重排了)。
两种校准器比起来:sigmoid 的 ECE 0.122 低于 isotonic 的 0.151、Brier 0.178 也略低于 0.180,和上面"小样本挑稳的那个"判断对上了。isotonic 没赢,恰恰是它太自由------57 行样本喂不饱那些台阶。
ROC-AUC 顺手解释一句:它量的是"排序"------随机抽一个真回来的人和一个真没回来的人,模型给前者更高分的概率。0.5 等于瞎猜,1 是完美排序。它不关心分数本身准不准,所以校准前后几乎不变,正好就是上面那句"改刻度不改排序"的度量。
看可靠性曲线的端点偏差(未校准时)
- 模型说 0.99 的那桶人,真实只 80% 回来(差 −0.19)→ 过自信。
- 模型说 0.51 的那桶人,真实 100% 回来(差 +0.49)→ 低估。
- 模型说 0.03 的那桶人,真实 44% 回来(差 +0.42)→ 低分端也飘。
校准后这些桶整体被拉回对角线附近。画出来就是下面这张------三条彩色线越贴黑虚线越好:

读图:横轴是模型预测概率(10 个等宽桶里的平均值),纵轴是那桶人实际回来的比例。黑色虚线是理想(贴着它走 = 分和真实一致)。未校准的红圈 在两头最飘:右侧说过自信、左侧也飘;sigmoid 的蓝方块 整体被掰回对角线附近,桶间连接也更顺;isotonic 的绿三角虽然更灵活但抖得厉害------0.64 那桶直接掉到 0,正是前面说过的"每个台阶都要样本撑,样本不够就过拟合"。
整体被往中间收这一点,看概率分布更直观:

读图:横轴是测试集 57 人的模型输出概率,纵轴是人数。**红色(未校准)**大多数人挤在 0.95~1.0、再一堆人挤在 0~0.1------典型的树模型两端的过自信;**蓝色(sigmoid 校准后)**那簇最高档被往下压了一点、低端那一簇基本被拉回到 0.4~0.9 区间------概率整体"往中间收"。
落到具体人:U0009 的满格误判
把测试集里的人摊开看,最扎眼的是 U0009。
在"未来"视角下(07-22 对模型是未知),U0009 早期(07-08、07-15)都是 label=1 的高频用户,模型没见过他凉,原始概率直接给到 1.000 (满格确信会回),但真实标签是 0(凉了)。这就是过自信:历史高频把模型骗了。
校准后:
- U0009(标签 0,真实没回):原始 1.000 → sigmoid 0.934 → isotonic 0.989
- U0025(标签 0,真实没回):原始 0.942 → sigmoid 0.773 → isotonic 0.761
- U0036(标签 1,几乎必回):原始 1.000 → sigmoid 0.929 → isotonic 0.989
U0025 这种"被高估"的人,校准把 0.94 明显拉到 0.77,偏差收窄;但 U0009 这种被推到满格的个例,单调校准只能微调(sigmoid 0.934 仍偏高)。说明两件事:
- 校准能把"整条曲线"调正,但救不了被特征本身骗到的极端个例。
- 真要抓 U0009 这种"转折用户",得靠更灵敏的特征------第12章那个全量模型正是靠趋势特征
f_trend把他读成 0.22 判对。校准是补概率可信度,不是补特征区分度。
段内校准:整体准,不代表每段都准
到这儿为止,校准都是在全体样本上做的:一个映射管所有人。整体指标看着不错(ECE 0.122),但把人拆开看,可能会发现某些子人群被系统性地高估或低估------第21章做偏置审计时就抓到过:低活跃人群被高估 0.231、久没来的人被高估 0.163。
用本示例复现一下这个现象。按"最近一次领取距今几天"把测试集分成两拨,看全局校准器在每拨上的表现:
| 段 | 人数 | 真实回访率 | 全局分均值 | 偏差 | 段内 ECE |
|---|---|---|---|---|---|
| 近期还来(recency ≤ 2) | 37 | 0.838 | 0.914 | +0.076 | 0.076 |
| 久没来(recency ≥ 3) | 20 | 0.550 | 0.692 | +0.142 | 0.142 |
两段都被高估,而且"久没来"那拨高估得更狠(说 69% 会回,实际只有 55%)。整体端得挺准的校准器,落到具体某拨人身上就偏了。
既然每段各有各的偏法,那给每段单独配一个校准器不就行了?思路是对的,真做了是这样(每段用自己那拨训练样本单独拟合一个 sigmoid 映射):
| 段 | 段内训练样本 | 全局 ECE | 段内校准后 ECE | 全局 Brier | 段内校准后 Brier |
|---|---|---|---|---|---|
| 近期还来 | 83 | 0.076 | 0.135 | 0.128 | 0.132 |
| 久没来 | 22 | 0.142 | 0.402 | 0.271 | 0.394 |
反而更糟了。 "久没来"那段崩得尤其厉害------ECE 从 0.142 涨到 0.402。
原因不复杂:那段在训练集里只有 22 行样本,拿 22 行去拟合一条映射,等于让校准器去背这 22 个人的运气。背出来的映射放到另外 20 个人身上,自然一塌糊涂。段内校准是个"用样本换精度"的买卖:每段得有足够多的人,才喂得起一个自己的校准器。本示例总共 162 行,两段一分,谁都不够。
所以真实的做法是分情况:
- 段够大(真实项目里按业务线、渠道、人群包分,每段成千上万人):段内校准值得做,段内决策就用段内分------"这段里取分数最低的 20% 去干预",用的是这段自己的刻度,准。
- 段小 :撑不起校准器,换个更省的办法------段内单独调阈值。校准器还是用全局那个,但"久没来"这拨人既然被系统性高估 0.14,那这拨人的干预线就相应抬高一点。一个阈值能解决的事,不用去拟合一整条曲线。
还有一条要记牢:段内分和段内分之间不能比 。每个段各掰各的刻度,拼起来的分表跨段就失去意义了------本例把两段的分拼回去,整体 Brier 从 0.178 涨到 0.224(虽然 AUC 碰巧从 0.698 升到 0.762,但那是掰刻度顺带改变了跨段排序的副作用,不是设计目标,也不可控)。结论是:段内做决策用段内分,跨段排一张总名单还得回到全局分。
校准的边界与坑
- 校准必须在独立验证集上做 :不能拿训练集既拟合模型、又拟合校准器、又评估,那会盲目乐观。本脚本用时间外推切分,加上
CalibratedClassifierCV内部再做交叉验证,双重防过拟合、防泄露(呼应第8章)。 - 校准不创造新信息:AUC、特征重要性都不变。它只让"分"更可信,便于运营按阈值或分档决策(比如"低于 30 分重点干预")。
- 校准器要随模型一起版本化、再训练(呼应第11章的版本化、第29章的监控再训练)。模型换了,旧的校准映射不一定还适用。
它和前后章节的关系
第14章讲模型在最小化对数损失(鼓励真概率),本章讲"鼓励"不够,还得显式校准这一步。下一章(第17章)把"训练 → 评估 → 每日评分"三步串起来真跑通,校准是评分前该做的一步------不校准,打出去的 0--100 分就名不副实。
动手
- 跑
附件/05_活跃度预测篇/activity_calibration.py,对照上面那张 Brier / ECE / AUC 表,再看可靠性曲线里未校准那几个偏差大的桶(0.99→0.80、0.51→1.00),看校准后怎么收窄;留意 isotonic 那桶 0.64→0.00,感受小样本过拟合。 - 跑
附件/05_活跃度预测篇/activity_calibration_viz.py生成两张配图:可靠性曲线对照三条线(红=未校准、蓝=sigmoid、绿=isotonic)怎么各自贴黑虚线、isotonic 在 0.64 桶怎么直接掉到 0;直方图看红色那一簇最高档被蓝色压回去多少、低端那一簇被收回到哪里。 - 看
activity_calibration.py脚本末尾代表用户三行:U0009(标签 0)原始给 1.000 却错了,校准后 0.934 仍高。想清楚"校准能纠整体偏移,但救不了被特征骗到的个例"。 - 跑
附件/05_活跃度预测篇/activity_segment_calibration.py,看那张"每段偏差"表------全局校准器在两段上都偏高(+0.076、+0.142),再看看段内校准怎么把 ECE 反而搞崩的。把分段阈值从recency <= 2改成<= 1重跑,段一大一小,看小那段崩得更厉害多少。 - 思考题:若直接拿未校准的 0.94(U0025)当"九成四把握"决定不干预,实际他没回;用校准后的 0.77 当依据,误判幅度小一成多。当样本量大、每天按分档自动决策,这种系统偏差会被放大成多少错判?这就是校准的业务意义。
本章配套脚本
附件/05_活跃度预测篇/activity_calibration.py(训练/测试时间外推切分 + GBDT + CalibratedClassifierCV 的 sigmoid/isotonic 两种校准 + Brier/ECE/AUC/可靠性曲线 + 代表用户逐个概率)、附件/05_活跃度预测篇/activity_calibration_viz.py(上述结果的两张可视化图:可靠性曲线 + 概率分布直方图)、附件/05_活跃度预测篇/activity_segment_calibration.py(段内校准:按段看全局分的偏差、每段单独拟合 sigmoid 并对比 ECE/Brier、段内分拼起来跨段不可比)。用到 scikit-learn、numpy、matplotlib,已在第4章附件/00_公共/requirements.txt列出,环境见第4章。训练表由第12章附件/05_活跃度预测篇/activity_train_table.py生成,口径一致。