🏠 总目录📚 本教程 15 · 采样与幸存者偏差
📑 本页目录(点开跳转)

15 · 采样偏差与幸存者偏差

52 分钟 | ⭐ 你的数据里只有「被系统选中过」的那些人


🎯 一句话

你的训练数据只包含被某个系统放行过的样本,而你要预测的是所有人。 上一章的泄漏是「多了不该有的信息」, 这一章是反过来 —— 少了一整块你从来没见过的人群,而且这块缺失不会报错。


✈️ 一、弹孔的故事,和它真正的重点

二战时统计学家看返航飞机的弹孔分布,建议加固弹孔最少的地方 —— 因为中弹在那里的飞机根本没飞回来。

   你看到的样本 =  被弹孔打中 AND 还能飞回来
   你想推断的   =  所有被弹孔打中的飞机

   ⭐ 故事的重点不是"要反着想",
      而是:【返航飞机的数据本身是完全正确的】。
      每一个弹孔都测量准确、没有脏数据、没有缺失、没有标注错误。

   💀 这套数据能通过你前面十章学的【全部】质量检查。

这是本章和第 04–08 章的分界线: 前面几章讲的是「数据是不是错的」, 这一章讲的是「数据全对,但它不是你以为的那群人」。 质量检查、契约、缺失分析 —— 一个都抓不到这个问题。


🎭 二、三个场景,同一个结构

场景 你有标签的是谁 你要预测的是谁 看不见的那块
招聘 入职并干满一年的人 所有投简历的人 被简历筛掉的人本来能不能干好 ⭐
风控 审批通过并放款的人 所有申请人 被拒绝的人本来会不会还钱
推荐 被曝光过的物品 全部候选物品 从没被推过的物品会不会被点
   三个场景剥开之后是同一件事:

     一个【旧的决策系统】 S 决定了谁能进入你的数据集
     你训练出的新模型 M,只在 S 放行过的人身上学过
     然后你要用 M 去替换 S ——
     ⭐ 也就是让它去决策一群它从来没见过的人

🔑 这个结构有个名字叫「反事实缺失」: 你缺的不是"数据量",是另一半世界的结果 —— 「如果当初批了他会怎样」这个问题,数据库里永远不会有答案。 ⚠️ 所以它不能靠"多攒几个月数据"解决:多攒的每一条,还是 S 放行过的。


🧊 三、先别慌:它什么时候【不】要紧

大多数人对选择偏差的直觉是过度悲观的。 先看一个实跑出来的反例。

模拟一个信贷场景:老策略只看 x1(收入类)和 x2(负债类)打分,批 40%。 新模型能看到 x1, x2, x3 三个特征(x3 老策略没用过但真的有预测力)。 只用「批过的人」训练:

评估对象 AUC
通过人群上评估 0.761
全体申请人上评估 0.821
上帝视角(假装所有人都有标签) 0.821

⭐⭐ 看第二行和第三行:一模一样。 在这个设定下,选择偏差没有造成任何损失。 条件是两个:① 老策略只依据可观测变量做决策;② 你的模型设定是对的。 满足这两条时,只在被选中的样本上训练不会带来系统性偏差 —— 这在统计里叫「基于可观测量的选择」。

⚠️ 但第一行是个大坑在通过人群上评估出来的 0.761 是偏低的, 真实能力是 0.821。这叫区间截断(range restriction) —— 你砍掉了最容易区分的那部分人(明显的坏客户全被老策略拒了), 剩下的都是"边缘案例",当然更难分。 ⭐ 所以:「我们模型的 AUC 只有 0.76」这句话,在有选择的人群上是没有意义的。


💥 四、什么时候它真的会害你:两种情况

情况 B:旧策略用了你看不到的东西

真实的审批不只有模型分,还有人工复核 —— 审批员看了流水照片、听了电话、感觉不对就拒了。 这些「感觉」既影响了谁被选中,也真的和还款相关,但它们不在你的特征里。

实跑(给旧策略加一个模型看不到的变量 uu 同时影响放行和还款):

评估 只用通过样本训练 上帝视角 缺口
全体申请人 AUC 0.759 0.778 0.019

情况 C:真实关系是非线性的,而选择切掉了一整段区间

这是最贵的一种,而且在业务里非常普遍:老策略把某个特征区间整段拒光了, 你的模型在那一段上从来没有一个样本,只能靠外推。

实跑(真实关系带一个二次项,老策略切掉了低分段):

评估对象 只用通过样本训练 上帝视角 缺口
全体申请人 0.813 0.841 0.028
被老策略拒掉的那段人群 0.772 0.829 0.057 💀

💀 看最后一行:在你最需要它的地方,模型差 0.057。 而「被老策略拒掉的人群」恰恰是新模型上线后要重新审视的人群 —— 因为如果新模型的结论和老策略完全一样,那它没有任何商业价值。 ⭐ 你部署新模型的全部意义,正好落在它最不可靠的那一段上。


🎲 五、留随机流量当尺子:代价与收益

唯一能真正补上那块缺失的办法,是故意放行一小撮本该被拒的样本。 业界叫法很多:随机流量、探索流量、reject inference 的黄金层、holdout 放款。

收益(同一组实验,随机放行被拒人群的 5%):

情况 无随机流量 5% 随机流量 上帝视角 补回了多少缺口
A(选择只看可观测量) 0.821 0.821 0.821 — (本来就没缺口)
B(不可观测混杂) 0.759 0.770 0.778 58%
C(区间被切断,在被拒人群上) 0.772 0.819 0.829 83% ⭐⭐

⭐⭐ 这张表是本章最有价值的三行5% 的随机流量,在"区间被切断"这种情况下补回了 83% 的能力缺口。 而在情况 A 里它一分钱效果都没有 —— 随机流量不是万能药,它只解决「你没见过的区间」这一个问题,但解决得非常彻底。

代价(同一份模拟里算出来的真实账):

   全体申请人的"好客户"比例 .............. 33.1%
   被老策略【通过】的人里好客户比例 ....... 54.2%
   被老策略【拒绝】的人里好客户比例 ....... 19.0%   ⭐ 这就是代价的来源

   随机放行被拒人群的 5%:
     覆盖全部申请人的 5% × 60% = 3.0%
     这 3% 里约 81% 是坏客户
     → 相对于全部放款量(40%),额外坏账敞口 ≈ 3.0% × 81% / 40% ≈ 6.1%
怎么把代价压下来 做法
限额 随机放行的样本给最低额度(比如正常额度的 10–20%),坏账敞口按额度线性缩小
分层抽样 ⭐⭐ 不要在全体被拒人群里均匀抽 —— 在紧邻阈值的那一段多抽,深度拒绝区少抽
时间稀释 不是每天都放,按周或按月放一批
只在需要的维度上探索 只对「老策略从没见过的特征组合」开口子

「分层抽样」这条能把代价砍掉一大半: 你真正需要信息的是决策边界附近,不是那些明显不该批的人。 在阈值上下 10 个百分位里抽样,用 1% 的敞口能换到接近 5% 均匀抽样的信息量


🧮 六、这一章唯一的一个公式:逆倾向加权(IPW)

如果你能估计出每个样本被选中的概率,就可以给样本加权,把分布掰回全体:

   记 π(x) = P(被选中 | x)          ← 叫"倾向分" propensity

   在【全体人群】上的期望损失,可以用【被选中的样本】估出来:

                    1        s_i
        E[ℓ]  ≈    ───  · Σ  ────  · ℓ_i
                    N       π(x_i)
                             ↑
                    被选中概率越低的样本,权重越大
                    (因为它代表了一大群没被选中的同类)

   s_i = 1 表示第 i 个样本被选中(有标签),Σ 只对这些样本求和
用它的前提 说明
π(x) 必须能估准 用「是否被选中」当标签训一个分类器就行,通常很好训
⚠️ 正性假设:每个 x 都有 π(x) > 0 💀 老策略硬阈值拒绝的区间里 π = 0,权重是无穷大 —— IPW 在那里彻底失效
⚠️ 选择必须只依赖可观测量 情况 B(不可观测混杂)下 IPW 也纠不回来

💀 正性假设是 IPW 最常被忽略的死穴,也是它和随机流量的分工所在老策略是硬规则(比如"征信查询次数>8 直接拒")时,那一段的 π 严格等于 0, 任何加权方法都无中生有不出信息。 ⭐ 随机流量的真正作用,就是把那些 π = 0 的区间变成 π = 0.05。 它买的不是样本量,是"正性"本身。

权重必须截断,否则方差爆炸

import numpy as np
from sklearn.linear_model import LogisticRegression

def ipw_weights(X_all, selected, clip=(0.02, 1.0)):
    """selected: 布尔数组,True 表示这一行有标签。返回被选中样本的权重"""
    pm = LogisticRegression(max_iter=1000).fit(X_all, selected.astype(int))
    pi = pm.predict_proba(X_all)[:, 1]
    pi = np.clip(pi, *clip)                    # ⭐ 不截断的话极小的 π 会产生天文数字权重
    w = 1.0 / pi[selected]
    w = w / w.mean()                           # ⭐ 归一化,便于和无权重的结果比较
    ess = w.sum() ** 2 / (w ** 2).sum()        # ⭐⭐ 有效样本量
    return w, {"有效样本量": round(float(ess)),
               "实际样本量": int(selected.sum()),
               "权重最大值": round(float(w.max()), 1)}

# ⭐⭐ 判读:有效样本量 < 实际样本量的 30% 时,IPW 的结论不值得相信 ——
#     说明你在用一小撮罕见样本去代表一大片人群

🔁 七、拒绝推断(reject inference)的四种做法

风控行业专门为这个问题起了名字。四种做法,质量从低到高

做法 怎么做 值不值
全部当坏客户 被拒的人一律标 bad 最差,等于把老策略的规则硬编码进新模型,新模型只会复制老策略
模糊标注 / 软标签 用当前模型给被拒样本打个概率,按概率拆成两条加权样本 ⚠️ 只是自证预言的一种精致写法:模型学的还是它自己的输出
IPW / 倾向加权 上面那个公式 ✅ 便宜、有效,但卡在正性假设上(硬拒绝区无解)
随机流量 ⭐⭐ 真的放行一小撮 ✅✅ 唯一能产生新信息的办法,也是唯一有成本的办法

一句话总结这张表前三种都是在已有信息里重新分配,只有第四种在制造新信息。 💀 前两种最危险的地方在于 —— 它们会让离线指标变好看, 因为你用来评估的测试集也是同一个偏差人群。 一个只会复制老策略的模型,在老策略的地盘上永远看起来很准。


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

   系统:某招聘平台的"简历-岗位"匹配打分模型
   目标:预测候选人投递后是否会【通过面试并入职】
   训练数据:过去 3 年的 HR 系统记录,41 万条投递

发生了什么

阶段 事件
建模 只有进入过面试环节的候选人才有"是否入职"的标签,占全部投递的 6.8%
处理方式 团队把「没进面试」的 93.2% 全部标成负样本 ⭐ 这就是「全部当坏客户」
离线 AUC 0.88,团队认为效果非常好
上线 用于简历排序,人工只看 Top 50
4 个月后 HR 反馈:推上来的人越来越像同一种人;入职转化率从 12.1% 降到 9.4%

为什么没被发现

   ① 93.2% 的负样本里,绝大多数只是【HR 没来得及看】,不是"不合适"  💀
      模型学到的其实是「HR 过去会不会点开这份简历」,
      而不是「这个人能不能干好这个活」

   ② 离线 AUC 0.88 是在【同一个偏差数据】上算的 ——
      模型完美复现了老 HR 的筛选习惯,所以它当然很准     ⭐

   ③ 4 个月里形成了反馈回路:
      模型推上来的人 → HR 面试 → 产生新标签 → 下次训练
      ⭐⭐ 越来越窄:第 4 个月推荐结果里,
         候选人的学校分布熵从 3.1 降到 2.2(降了 29%)

代价

   入职转化率        12.1%  →  9.4%          (相对下降 22%)
   月均入职           约 340 人  →  264 人
   4 个月少入职       约 300 人
   ⭐ 更贵的是不可见的部分:
      被系统性排在后面的那批候选人,永远不会产生"其实他很好"的证据 ——
      这个损失【在任何指标上都看不到】

该补什么

缺失 补上之后
把"没被看"当成负样本 💀 区分三类:面试后拒 / 面试后录用 / 从未被看(后者不进训练集,或用 IPW 加权)
没有随机曝光位 ⭐ Top 50 里固定留 3 个随机位(从模型排名 50 名开外随机抽)。代价 6%,用来当尺子
离线评测集和训练集同源 用随机位产生的数据单独建一个无偏评测集第 13 章
没有多样性监控 学校/专业/工作年限的分布熵进日报,掉 > 10% 告警
没人问"看不见的那部分" 模型评审新增一问:「哪些人不会出现在你的训练数据里?」

改进后的关键数字:加上 3 个随机位(6% 的展示量)之后, 用随机位数据评估出来的真实 AUC 是 0.71,不是 0.88。 那 0.17 的差距,就是"模型复制老 HR"和"模型真的会看人"之间的距离。


🧭 九、一张判断表:要不要处理,怎么处理

先问这个 如果是 该做
我的标签是不是只有"被某个系统放行的样本"才有? ✅ 不用管这一章
老策略是不是只依据我也能看到的变量做决策? ⭐ 偏差可能为零,但评估必须在全体人群上做,别被截断区间的低分吓到
老策略里有没有人工判断 / 我看不到的字段 ⚠️ 情况 B,IPW 帮不上,只能靠随机流量
老策略有没有硬规则整段拒绝 💀 情况 C,π = 0,任何加权都无效,必须随机流量
新模型的价值是不是主要来自"改判老策略拒掉的人"? ⭐⭐ 那就必须有随机流量,否则你的评测在最关键的区间上完全空白
系统的输出会不会影响下一批训练数据? ⚠️ 反馈回路,问题会逐月加重,必须有分布熵之类的监控

最后一行是最容易被忽略的: 选择偏差如果不管,它不是一个静态的偏差,它会自我加强今天窄一点点,三个月后窄很多 —— 招聘那个案例里,四个月熵掉了 29%。


🔗 这一章连到哪里

去哪 为什么
模型上线之后 13 不能做 AB 时的因果推断 ⭐ IPW、倾向分、反事实这一整套工具的完整版。这一章只用了它最简单的一个形式
推荐算法 13 冷启动与探索利用 推荐场景的同一个问题:没被曝光过的物品永远拿不到反馈。那边讲怎么系统性地分配探索预算
强化学习基础 07 探索与利用 「留随机流量」在 RL 里就是 ε-greedy。这一章的代价/收益权衡,那边有完整的理论版本
模型上线之后 14 辛普森悖论与七个陷阱 另一类「数据全对但结论是错的」问题,和本章是同一个家族

✅ 检查点

  1. 弹孔故事的真正重点是什么?为什么前面几章的质量检查抓不到这个问题?
  2. 招聘、风控、推荐三个场景剥开之后是同一个什么结构?为什么"多攒几个月数据"解决不了?
  3. 选择偏差什么时候要紧?实跑数字是多少?这时候还有什么坑?
  4. 什么是区间截断?为什么"我们模型 AUC 只有 0.76"这句话在有选择的人群上没有意义?
  5. 选择偏差真正会害你的两种情况是什么?情况 C 在被拒人群上的缺口是多少?为什么这个缺口特别致命?
  6. 5% 随机流量在三种情况下各补回了多少缺口?代价怎么算、怎么压?
  7. IPW 的公式是什么?正性假设为什么是它的死穴?有效样本量该怎么判读?
  8. 拒绝推断四种做法里,为什么说前三种都不产生新信息?前两种最危险的地方在哪?
  9. 招聘那个事故里,团队怎么处理没进面试的样本?模型实际学到了什么?代价是多少?加上随机位之后真实 AUC 是多少?
  10. 为什么说选择偏差"不是静态的"?案例里的数字是多少?
👀 答案
  1. 重点是返航飞机的数据本身完全正确——每个弹孔都测量准确,没有脏数据、缺失、标注错误,这套数据能通过前面十章的全部质量检查。因为问题不是"数据是错的",而是"数据全对,但它不是你以为的那群人"
  2. 结构是:一个旧的决策系统 S 决定了谁能进入你的数据集,新模型 M 只在 S 放行过的人身上学过,然后你要用 M 去决策一群它从没见过的人。多攒数据没用是因为这叫反事实缺失——缺的不是数据量而是"另一半世界的结果",多攒的每一条还是 S 放行过的
  3. ① 老策略只依据可观测变量决策 ② 模型设定正确 时不要紧。实跑:只用通过样本训练,在全体申请人上 AUC 0.821,和上帝视角完全相同。坑在于:在通过人群上评估出来的 0.761 是偏低的,这叫区间截断。
  4. 区间截断:老策略把最容易区分的那部分人(明显的坏客户)拒掉了,剩下的都是边缘案例,所以看起来更难分。所以在有选择的人群上报出来的 AUC 既不代表真实能力也不能横向比较——真实能力是 0.821,报出来是 0.761。
  5. B:旧策略用了你看不到的东西(人工复核的"感觉",既影响放行也影响还款),缺口 0.019;C:真实关系非线性 + 选择切掉了一整段特征区间,全体上缺口 0.028,但在被老策略拒掉的人群上缺口是 0.057。致命是因为被拒人群恰恰是新模型上线后要改判的人群——如果新模型结论和老策略一样,它就没有商业价值,所以你部署它的全部意义正好落在它最不可靠的那一段上
  6. A 补回 0(本来就没缺口)、B 补回 58%(0.759→0.770,上帝 0.778)、C 在被拒人群上补回 83%(0.772→0.819,上帝 0.829)。代价:被拒人群里只有 19.0% 是好客户(通过人群是 54.2%,全体 33.1%),随机放行被拒的 5% ≈ 全体的 3%,其中 81% 是坏客户,额外坏账敞口约 6.1%。压成本靠:给最低额度(敞口线性缩小)、⭐⭐在紧邻阈值那一段分层抽样(1% 敞口换到接近 5% 均匀抽样的信息量)、时间稀释、只对老策略没见过的特征组合开口。
  7. E[ℓ] ≈ (1/N) Σ (s_i / π(x_i)) · ℓ_i,被选中概率越低的样本权重越大。正性假设(每个 x 都要 π(x) > 0)是死穴:老策略硬阈值拒绝的区间里 π 严格等于 0,权重无穷大,IPW 在那里彻底失效——⭐ 随机流量的真正作用就是把 π = 0 的区间变成 π = 0.05,它买的不是样本量,是"正性"本身。有效样本量 (Σw)² / Σw²低于实际样本量的 30% 时结论不值得相信
  8. 因为前三种都只是在已有信息里重新分配,只有随机流量在制造新信息。前两种(全部当坏客户 / 软标签)最危险的是它们会让离线指标变好看——评估用的测试集也是同一个偏差人群,一个只会复制老策略的模型,在老策略的地盘上永远看起来很准
  9. 93.2% 没进面试的投递全部标成负样本(即「全部当坏客户」),而这些人绝大多数只是HR 没来得及看。模型实际学到的是「HR 过去会不会点开这份简历」,不是"这个人能不能干好"。代价:入职转化率 12.1% → 9.4%(相对降 22%),4 个月少入职约 300 人,学校分布熵从 3.1 降到 2.2。加 3 个随机位(6% 展示量)后测出真实 AUC 是 0.71 而不是 0.88——那 0.17 就是"复制老 HR"和"真的会看人"之间的距离
  10. 因为存在反馈回路:模型推谁 → 谁被面试 → 产生新标签 → 下次训练,范围越来越窄,会自我加强。案例里 4 个月学校分布熵从 3.1 降到 2.2,掉了 29%。所以必须把分布熵一类的多样性指标放进日报,掉超过 10% 就告警。

🛑 可以停在这里

走神救援

⭐⭐核心:你的训练数据只包含"被某个旧系统放行过"的样本,而你要预测的是所有人。 上一章泄漏是"多了不该有的信息",这一章是少了一整块你从没见过的人群,而且不会报错。⭐弹孔故事的真正重点不是"反着想",而是返航飞机的数据本身完全正确——每个弹孔都测量准确、没有脏数据没有缺失,它能通过前十章的全部质量检查。这就是本章和 04–08 章的分界:那些讲"数据是不是错的",这章讲"数据全对,但它不是你以为的那群人"招聘/风控/推荐是同一个结构:旧系统 S 决定谁进数据集 → 新模型 M 只在 S 放行的人身上学 → 你却要用 M 去决策 M 没见过的人。⚠️这叫反事实缺失,多攒几个月数据没用,因为多攒的每一条还是 S 放行的。⭐先别慌,它不总是有害:实跑显示当①旧策略只依据可观测变量②模型设定正确时,只用通过样本训练,全体人群 AUC 0.821 = 上帝视角 0.821,零偏差。⚠️但有个大坑叫区间截断:在通过人群上评出来只有 0.761——老策略拒掉了最好分的那批人,剩下全是边缘案例,所以"我们 AUC 只有 0.76"这句话在有选择的人群上没有意义。💥真正会害你的两种B 旧策略用了你看不到的东西(人工复核的"感觉"同时影响放行和还款)缺口 0.019;C 真实关系非线性 + 选择把一整段特征区间切没了——全体上缺口 0.028,但在被老策略拒掉的人群上缺口高达 0.057 💀。C 特别致命,因为被拒人群恰恰是新模型要改判的人群——如果新模型结论和老策略一样它就没商业价值,所以部署它的全部意义正好落在它最不可靠的那一段上。🎲随机流量的收益(实跑):5% 随机放行在 A 补回 0、B 补回 58%C 在被拒人群上补回 83%(0.772→0.819,上帝 0.829) ⭐⭐——随机流量不是万能药,它只解决"没见过的区间"这一个问题,但解决得非常彻底。代价:被拒人群好客户率只有 19.0%(通过人群 54.2%、全体 33.1%),随机放行被拒的 5% 约等于全体的 3%、其中 81% 是坏客户、额外坏账敞口约 6.1%。⭐压成本三招:给最低额度、⭐⭐在紧邻阈值那一段分层抽样(1% 敞口换到接近 5% 均匀抽样的信息量)、时间稀释。🧮唯一的公式 IPWE[ℓ] ≈ (1/N)·Σ s_i/π(x_i)·ℓ_i,π 是被选中概率,选中概率越低权重越大。💀它的死穴是正性假设:老策略硬阈值拒绝的区间里 π 严格等于 0,任何加权都无中生有不出信息——⭐随机流量的真正作用就是把 π=0 变成 π=0.05,它买的不是样本量,是"正性"本身。⭐⭐IPW 必须截断权重并看有效样本量 (Σw)²/Σw²,低于实际样本量 30% 就别信这个结论拒绝推断四种做法:全部当坏客户(❌最差,把老策略硬编码进新模型)/ 软标签(⚠️自证预言的精致写法)/ IPW(✅便宜但卡在正性)/ 随机流量(✅✅唯一产生新信息的,也是唯一有成本的)。⭐前三种都只是在已有信息里重新分配;💀前两种最危险的是它们会让离线指标变好看——测试集也是同一个偏差人群,只会复制老策略的模型在老策略的地盘上永远看起来很准。💀招聘事故:只有 6.8% 的投递进过面试才有标签,团队把 93.2% 没进面试的全标成负样本——而这些人绝大多数只是 HR 没来得及看,于是模型学到的是"HR 会不会点开这份简历",不是"这人能不能干好"。离线 AUC 0.88,上线 4 个月后入职转化率 12.1% → 9.4%(相对降 22%,少入职约 300 人),学校分布熵 3.1 → 2.2(掉 29%)——反馈回路让它逐月变窄。补救:区分"面试后拒/面试后录用/从未被看"三类、Top 50 里留 3 个随机位(6% 代价)、用随机位数据建无偏评测集、分布熵进日报掉 10% 告警、评审必问一句⭐「哪些人不会出现在你的训练数据里?」。⭐改进后的关键数字:用随机位数据测出的真实 AUC 是 0.71 而不是 0.88——那 0.17 就是"复制老 HR"和"真的会看人"之间的距离。 最后记住:⚠️选择偏差不是静态的,它会自我加强,今天窄一点点,三个月后窄很多。

下一节 👉 16-合成数据.md

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