🏠 总目录📚 本教程 12 · 评估与A/B实验
📑 本页目录(点开跳转)

12 · 评估与 A/B 实验

25 分钟 | ⭐⭐ 工作中用得最多的一节


🎯 一句话

离线指标决定你上不上线,在线实验决定你留不留下。 两者经常不一致——理解「为什么不一致」,是初级和高级工程师最大的分水岭。


📊 第一部分:离线指标

分成三类,别搞混

① 评分预测类(越来越少用)
  • RMSE, MAE
  • 👉 只在显式评分场景有意义。工业界基本不用了
② 分类类(精排用)⭐
  • AUC, GAUC, LogLoss
  • 👉 衡量「模型打出来的分数好不好」
  • ⚠️ AUC/GAUC 管排序,LogLoss 管概率数值——不是一件事
③ 排序/召回类(召回和端到端用)⭐
  • Recall@K, Precision@K, NDCG@K, MRR, HitRate@K
  • 👉 衡量「排出来的列表好不好」

⚠️ 第 ② 类里藏着一个高频混淆,先说清再往下看:AUC / GAUC 只看「正样本有没有排在负样本前面」, 所以对分数做任何单调变换(全体乘 0.1、全体加 0.05)AUC 纹丝不动——它对「概率的绝对值准不准」是瞎的。 一个把 pCTR 整体高估一倍的模型,AUC 可以毫无变化。

只关心「先给谁看」的推荐场景无所谓;但一旦要把这个分数乘进钱里、或者拿它卡阈值做决策, 你要看的就是 LogLoss 和校准曲线,而不是 AUC。 🔗 这件事的完整后果(含一次 pCTR 高估导致的真实事故)在 15b · 广告:从推荐到竞价 第七节; 校准怎么量(ECE、校准表、Platt / isotonic)在 《模型上线之后》02 · 离线好不等于线上好 第七节。


⭐ AUC:精排的第一指标

定义:随机取一个正样本和一个负样本,模型给正样本打分更高的概率。

   AUC = 0.5   → 瞎猜
   AUC = 0.7   → 推荐系统里算差
   AUC = 0.75  → 一般
   AUC = 0.80  → 不错
   AUC = 0.85+ → 很好(或者……你穿越了,去查特征)⚠️

   💡 推荐系统里 AUC 提升 0.001 (0.1个千分点) 就是有意义的改进
      别拿 CV/NLP 的直觉来看这个数字

🔥 GAUC:比 AUC 更该看的指标

AUC 的致命问题:它把所有用户混在一起算。

  用户A(重度用户):点了 100 个,预测分都在 0.8-0.9
  用户B(轻度用户):点了 2 个,预测分都在 0.1-0.2

  全局 AUC 很高 ✅  —— 因为 A 的正样本分数 > B 的负样本分数
  但这个"高"毫无意义 —— 你从来不需要比较 A 和 B 的物品!

GAUC = 按用户分组算 AUC,再加权平均

$$\text{GAUC} = \frac{\sum_u w_u \cdot \text{AUC}_u}{\sum_u w_u}, \quad w_u = \text{用户 } u \text{ 的曝光数}$$

💡 人话「在每个用户自己的候选列表里,模型排得对不对?」 —— 这才是线上真实发生的事。

import numpy as np
from sklearn.metrics import roc_auc_score

def gauc(user_ids, y_true, y_pred):
    total_w, total_auc = 0.0, 0.0
    for u in np.unique(user_ids):
        m = user_ids == u
        yt = y_true[m]
        if len(np.unique(yt)) < 2:      # 全正或全负,算不了 AUC,跳过
            continue
        w = m.sum()
        total_auc += w * roc_auc_score(yt, y_pred[m])
        total_w += w
    return total_auc / total_w if total_w else 0.0

📌 面试高频:「AUC 和 GAUC 的区别?为什么推荐系统更看 GAUC?」 答案就是上面那段。


⭐ NDCG:排序质量的黄金标准

为什么需要它:Precision@10 认为「排第 1」和「排第 10」一样好。显然不对。

  DCG@K = Σ  (2^rel_i - 1) / log₂(i + 1)
          i=1..K
               ↑             ↑
           物品的相关度    位置折损(越靠后贡献越小)

  NDCG@K = DCG@K / IDCG@K     ← 除以"理想排序"的 DCG,归一化到 0~1
def ndcg_at_k(ranked_relevances, k=10):
    """ranked_relevances: 按模型排序后,每个位置的真实相关度"""
    r = np.asarray(ranked_relevances, dtype=float)[:k]
    discounts = np.log2(np.arange(2, len(r) + 2))
    dcg = np.sum((2 ** r - 1) / discounts)

    ideal = np.sort(np.asarray(ranked_relevances, dtype=float))[::-1][:k]
    idcg = np.sum((2 ** ideal - 1) / np.log2(np.arange(2, len(ideal) + 2)))
    return dcg / idcg if idcg > 0 else 0.0

各指标速查

指标 公式直觉 什么时候用
Recall@K 相关物品有多少被召回了 ⭐ 召回阶段的主指标
Precision@K 推的 K 个里有多少是对的 展示位有限时
HitRate@K 前 K 个里有没有命中(0/1) 序列推荐常用
MRR 第一个正确答案的位置的倒数 只关心第一个命中
NDCG@K 考虑位置和相关度等级 ⭐ 排序质量的通用标准
MAP 各位置 Precision 的平均 多个正样本时
AUC / GAUC 正样本排在负样本前的概率 ⭐ 精排 CTR 模型
LogLoss 概率预测的准确度 需要概率校准时(如广告计费)

🕳️ 离线评估的五个致命陷阱 ⭐⭐

陷阱 1:随机切分数据集

用未来预测过去 → 离线指标虚高。 ✅ 按时间切分,或用 Leave-One-Out(留最后一次)。

陷阱 2:负采样评测

(第 9 节讲过)采 100 个随机负样本评测 → 指标虚高且模型排名会错。 ✅ 全量排序评测

陷阱 3:曝光偏差(Exposure Bias)⭐ 最根本的问题

  你的测试集里只有「系统曾经推过的物品」。
  系统从没推过的物品,用户会不会喜欢?——你的数据里没有答案。

  → 离线评估实际上是在问「新模型有多像老模型」
  → 一个和老模型完全不同但更好的模型,离线指标可能反而更差!

部分缓解: - 留 1% 流量做随机曝光,收集无偏数据集专门用于评估 - 反事实评估 / Off-Policy Evaluation:IPS(倾向得分加权)、Doubly Robust

陷阱 4:位置偏差

点击率里混着「因为排在第一位所以被点」的成分。 ✅ 训练时把位置作为特征输入,预测时统一填固定值。

陷阱 5:只看总体指标

总体 AUC 涨了,但可能是:新用户跌了、老用户涨了;或者热门涨了、长尾跌了。 ✅ 必须分组看:新/老用户、活跃度分层、热门/长尾物品、不同类目。


🧪 第二部分:A/B 实验(线上才算数)

基本原理

  全部用户
     │
     ├──── 50% ────► 【对照组 Control】  跑老策略
     │
     └──── 50% ────► 【实验组 Treatment】跑新策略
                            │
                     跑 7-14 天
                            │
                     比较核心指标
                            │
                  统计显著? → 全量上线 / 回滚

关键前提:分流必须随机且稳定

# 标准做法:哈希用户 ID,保证同一用户始终进同一组
bucket = int(hashlib.md5(f"{exp_id}_{user_id}".encode()).hexdigest(), 16) % 100
group = "treatment" if bucket < 50 else "control"

⚠️ 注意 exp_id 要参与哈希,否则多个实验会用同一个分桶 → 实验之间互相污染。


📐 分层实验 / 流量复用(Google Overlapping Experiments)

问题:公司有 200 个实验要同时跑,流量不够分。

Layer 1: 召回层 实验A(50%) | 实验B(50%)
Layer 2: 精排层 实验C(30%) | 对照(70%)
Layer 3: 重排层 实验D(20%) | 对照(80%)

同一个用户可以同时命中 A、C、D 每层独立随机哈希(用不同的 salt)→ 层间正交,互不影响

⚠️ 同一层内的实验必须互斥(不能一个用户同时跑两个召回实验)。


📈 该看什么指标(分三档)

  【核心指标】决定上不上线,1-2 个就够
    · 人均使用时长
    · 次日/7日留存
    · 人均消费内容数
    · GMV / 转化率(电商)

  【护栏指标 Guardrail】不能跌,跌了一票否决 ⭐
    · 页面加载延迟 P99
    · 崩溃率、错误率
    · 内容多样性
    · 举报率 / 负反馈率
    · 创作者侧指标(新作者曝光量)
    · 收入(如果实验不是为了提收入)

  【诊断指标】用来解释「为什么涨/跌」
    · CTR、完播率、点赞率
    · 各召回路的贡献占比
    · 各类目的曝光分布

🚨 护栏指标是最容易被新人忽略、也最容易出事故的地方。 一个 CTR +3% 但延迟 +200ms、举报率 +50% 的实验,必须回滚


🔢 统计显著性(别跳过这段)

问题:CTR 从 5.0% 涨到 5.1%,是真的涨了还是随机波动?

import numpy as np
from scipy import stats

def ab_test(clicks_a, imps_a, clicks_b, imps_b, alpha=0.05):
    """两组转化率的显著性检验(双比例 z 检验)"""
    p_a, p_b = clicks_a / imps_a, clicks_b / imps_b
    p_pool = (clicks_a + clicks_b) / (imps_a + imps_b)
    se = np.sqrt(p_pool * (1 - p_pool) * (1 / imps_a + 1 / imps_b))
    z = (p_b - p_a) / se
    p_value = 2 * (1 - stats.norm.cdf(abs(z)))

    # 相对提升 + 95% 置信区间
    lift = (p_b - p_a) / p_a
    se_diff = np.sqrt(p_a*(1-p_a)/imps_a + p_b*(1-p_b)/imps_b)
    ci = (( (p_b-p_a) - 1.96*se_diff)/p_a, ((p_b-p_a) + 1.96*se_diff)/p_a)

    return {
        "control": f"{p_a:.4%}", "treatment": f"{p_b:.4%}",
        "lift": f"{lift:+.2%}", "lift_95CI": f"[{ci[0]:+.2%}, {ci[1]:+.2%}]",
        "p_value": round(p_value, 5),
        "significant": p_value < alpha,
    }

print(ab_test(clicks_a=50_000, imps_a=1_000_000,
              clicks_b=51_000, imps_b=1_000_000))

需要多少样本? 先算清楚再开实验:

def sample_size(baseline_rate, mde, alpha=0.05, power=0.8):
    """
    mde: 最小可检测提升(相对值),如 0.02 表示想检出 2% 的相对提升
    返回:每组需要多少样本
    """
    from scipy.stats import norm
    p1 = baseline_rate
    p2 = baseline_rate * (1 + mde)
    z_a, z_b = norm.ppf(1 - alpha/2), norm.ppf(power)
    p_bar = (p1 + p2) / 2
    n = (z_a*np.sqrt(2*p_bar*(1-p_bar)) + z_b*np.sqrt(p1*(1-p1)+p2*(1-p2)))**2 / (p2-p1)**2
    return int(np.ceil(n))

# CTR 5%,想检出 2% 的相对提升
print(sample_size(0.05, 0.02))   # 每组需要约 75 万曝光

💡 这个计算能救你:如果算出来需要 3 个月才能攒够样本,那这个实验就别做了, 或者换一个更敏感的指标 / 更大的流量。


🚨 A/B 实验的七宗罪

描述 后果
1. 偷看结果 (Peeking) 每天看一眼,一显著就停 假阳性率从 5% 飙到 30%+
2. 多重比较 看 50 个指标,总有几个显著 至少要做 Bonferroni 校正
3. 新奇效应 新东西前几天就是新鲜 至少跑满 1 个完整周(含周末)
4. 分流不均 AA 测试都有差异 每次实验先跑 AA 测试验证
5. 网络效应 社交产品里实验组影响对照组 改用聚类分流/时间片轮转
6. 幸存者偏差 只看留下的用户,流失的没算 分母要用实验开始时的全部用户
7. 只看短期 标题党短期 CTR 高,长期掉留存 做长期实验 / 反转实验

⭐ 必做:AA 测试

在正式实验前,把用户随机分两组,两组跑完全一样的策略

如果 AA 测试就发现显著差异 → 你的分流系统有 bug,这时候做的任何 A/B 实验都是垃圾。

⭐ 推荐:反转实验(Holdback)

新策略全量上线后,留 1–5% 的用户永远跑老策略,持续观察几个月。

这是唯一能检测「长期效应」的方法——很多策略短期涨、三个月后掉。


🔄 离线和在线不一致,怎么办?(真实工作流程)

   离线 AUC 涨了 0.3%,上线 CTR 没动 —— 怎么排查?

   ① 检查特征穿越      → 离线用了线上拿不到的特征?
   ② 检查离线/在线一致  → 同一个样本,两边打分一样吗?(必查!)
   ③ 检查候选集差异    → 离线在小候选集上评,线上是全库?
   ④ 检查约束差异      → 线上有多样性/频控/广告位,离线没有?
   ⑤ 检查流量分布      → 离线数据是历史全量,线上是当前时段?
   ⑥ 检查样本量        → 实验跑够了吗?置信区间包含 0 吗?

🔧 第 ② 条是最常见的原因,也最容易查: 拿 1000 条线上真实请求,用离线模型跑一遍,对比打分。差异 > 1e-5 就是有问题。 这个检查叫 「一致性校验 / Consistency Check」,应该做成上线前的自动化流程。


🔗 这一章连到哪里

去哪为什么
上线之后 12A/B 的深水区:peeking 为什么把假阳性推到 30%、SRM(样本比例失衡)怎么查、多重比较怎么校正
上线之后 13有网络效应或改动无法分流时,A/B 做不了——用 DID / 合成控制 / 断点回归代替
上线之后 14陷阱 5「只看总体指标」的完整版:每个分组都涨、总体却跌,是怎么发生的

✅ 检查点

  1. AUC 和 GAUC 的区别?为什么推荐系统更看 GAUC?
  2. NDCG 比 Precision@K 好在哪?
  3. 什么是曝光偏差?它为什么让离线评估从根本上不可靠?
  4. 护栏指标是什么?举 3 个例子。
  5. 什么是 AA 测试?为什么必须做?
  6. 「偷看结果」为什么危险?
  7. 离线涨、线上不涨,你会先查什么?
👀 答案 1. AUC 把所有用户混在一起算,会奖励「区分不同用户」这种线上用不到的能力。GAUC 按用户分组算再加权平均,衡量的是「在每个用户自己的候选列表里排得对不对」,这才是线上真实场景。 2. NDCG 有位置折损(排第 1 比排第 10 贡献大)和相关度等级,Precision@K 把 K 个位置一视同仁。 3. 训练/测试数据里只有系统曾经推过的物品,没推过的没有标签。所以离线评估实际在衡量「新模型有多像老模型」,一个更好但不同的模型可能离线指标反而差。 4. 不能变差的底线指标。例:延迟 P99、崩溃率、举报率、多样性、创作者曝光、收入。 5. 两组跑完全一样的策略。如果 AA 就显著差异,说明分流系统有 bug,之后所有实验结论都不可信。 6. 每天看、一显著就停 → 相当于做了多次检验,假阳性率从 5% 涨到 30%+。应预先定好样本量和实验时长。 7. 先查离线/在线一致性校验(拿线上真实请求用离线模型跑,对比打分)。这是最常见也最容易查的原因。

🛑 可以停在这里

走神救援

离线指标:精排看 GAUC(分用户算,比 AUC 靠谱),召回看 Recall@K,排序质量看 NDCG。五大陷阱:随机切分、负采样评测、曝光偏差(最根本)、位置偏差、只看总体。在线 A/B:分层实验复用流量、核心+护栏+诊断三档指标、必做 AA 测试、别偷看结果、跑满一周、用反转实验看长期。离线在线不一致先查一致性校验。先算样本量再开实验:CTR 5% 想检出 2% 的相对提升,每组要约 75 万曝光;如果算出来得跑三个月,这个实验就不该做——换更敏感的指标或更大流量。⭐ 分流哈希必须带上 exp_id,否则多个实验共用同一套分桶、结论互相污染;分层实验里同一层内的实验必须互斥,层与层之间用不同 salt 才正交。⚠️ 护栏指标是一票否决项,不是参考项:CTR +3% 但 P99 延迟 +200ms、举报率 +50% 的实验,必须回滚。⚠️ 别拿 CV/NLP 的数量级直觉看 AUC——推荐里 AUC 涨 0.001(一个千分点)就是有意义的改进,反过来看到 0.85+ 先怀疑特征穿越。⭐ 曝光偏差治不好只能缓解:留 1% 随机曝光流量攒一份无偏评估集,或用 IPS / Doubly Robust 做反事实评估

下一节 👉 13-冷启动与探索利用.md

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