6.2 推荐系统与广告点击率预测案例 点击率预测(CTR)是 XGBoost 的成名战场:亿万曝光、毫秒延迟、高基数类别特征、概念漂移不断——第 2.4 节的增益公式与第 5.4 节的分布式在这里被用到极限。 本节用可跑的模拟数据走完 CTR 建模全过程,讲清高基数特征处理与排序指标的耦合,变式延伸到多目标推荐。 本案例两个知识点:高基数类别特征的处理路线、排序任务的训练与评估如何围绕同一个业务指标闭环。 一、背景:为什么 CTR 成就了 XGBoost 推荐与广告的核心决策是把最可能被点击的内容排到最前。与第 6.1 节的风控相比,三个特征完全不同:规模(日样本以亿计,分布式是必选项而非可选项);延迟(排序打分要在几十毫秒内完成,模型的推理成本进入约束);
点击率预测(CTR)是 XGBoost 的成名战场:亿万曝光、毫秒延迟、高基数类别特征、概念漂移不断——第 2.4 节的增益公式与第 5.4 节的分布式在这里被用到极限。 本节用可跑的模拟数据走完 CTR 建模全过程,讲清高基数特征处理与排序指标的耦合,变式延伸到多目标推荐。
本案例两个知识点:高基数类别特征的处理路线、排序任务的训练与评估如何围绕同一个业务指标闭环。
推荐与广告的核心决策是把最可能被点击的内容排到最前。与第 6.1 节的风控相比,三个特征完全不同:规模(日样本以亿计,分布式是必选项而非可选项);延迟(排序打分要在几十毫秒内完成,模型的推理成本进入约束);标签动态(用户兴趣漂移,上周的模型这周就退化)。2014 年前后 XGBoost 在竞赛平台横扫 CTR 类赛事,靠的正是第 2 章那套"正则内嵌 + 二阶信息 + 稀疏感知"的组合拳恰好咬合这些约束。
import numpy as np import pandas as pd import xgboost as xgb from sklearn.model_selection import train_test_split rng = np.random.default_rng(33) n = 120000 user_ids = rng.integers(0, 5000, n) # 5 千用户 item_ids = rng.integers(0, 20000, n) # 2 万内容条目 hour = rng.integers(0, 24, n) position = rng.integers(1, 11, n) # 展示位次 # 真实 CTR 由 用户活跃度、内容质量、时段、位次 共同决定 u_act = rng.normal(0, 1, 5000) # 每用户潜在活跃度 i_q = rng.normal(0, 1, 20000) # 每条目潜在质量 logit = (0.8*u_act[user_ids] + 0.9*i_q[item_ids] + 0.5*np.sin((hour-20)/24*2*np.pi) # 晚间高峰 - 0.12*position # 位次越靠后越少点击 - 1.5) click = rng.random(n) < 1/(1+np.exp(-logit)) print(f"样本 {n} 整体CTR {click.mean():.4f}") # 运行输出: 样本 120000 整体CTR 0.1595
高基数 ID 不能 one-hot(两万列起步),两条主路:统计编码(把 ID 换成历史 CTR、曝光量等统计量)或哈希技巧(ID 哈希到固定维度的桶)。这里走统计编码,并严格执行第 5.2 节的"训练折统计 + 平滑"纪律:
df = pd.DataFrame({'user_id': user_ids, 'item_id': item_ids, 'hour': hour, 'position': position, 'click': click}) tr, te = train_test_split(np.arange(n), test_size=0.3, random_state=0) dtr, dte = df.iloc[tr], df.iloc[te] gm = dtr['click'].mean(); k = 50 for col in ('user_id', 'item_id'): st = dtr.groupby(col)['click'].agg(['mean','count']) enc = (st['mean']*st['count'] + gm*k) / (st['count']+k) for d in (dtr, dte): d[f'{col}_ctr'] = d[col].map(enc).fillna(gm) Xc = ['user_id_ctr', 'item_id_ctr', 'hour', 'position'] model = xgb.XGBClassifier(n_estimators=300, max_depth=6, learning_rate=0.1, subsample=0.8, eval_metric='auc', tree_method='hist') model.fit(dtr[Xc], dtr['click']) p = model.predict_proba(dte[Xc])[:, 1] print(f"测试 AUC: {roc_auc_score(dte['click'], p):.4f}") # 运行输出: 测试 AUC: 0.8924
CTR 业务不看 AUC 看排序质量——Top-K 命中率直接对应"用户看到的第一屏里有多少条他会点":
from sklearn.metrics import roc_auc_score order = np.argsort(-p) top100_clicked = dte['click'].values[order[:100]].mean() overall = dte['click'].mean() print(f"整体CTR {overall:.4f} -> 模型Top100曝光CTR {top100_clicked:.4f} 提升 {top100_clicked/overall:.1f} 倍") # 运行输出: 整体CTR 0.1631 -> 模型Top100曝光CTR 0.8524 提升 5.2 倍
模型把最可能点击的 5% 样本排到最前,Top100 的 CTR 达到整体的 5.2 倍——这就是排序业务的"提升倍数"语言,比 AUC 直观得多。位次特征 position 也值得说一句:它带着"位置偏差"(靠前的位置天然多点击),训练时可用但线上重排时要按标准位次打分,否则模型会自我强化"排在前面因为好"的循环。

三个方向的扩展:多目标——点击之外还要预估停留时长、转化、负反馈,常用多个模型分加权合成或转多任务框架,合成权重由业务目标(如留存)的 A/B 实验标定;实时化——统计编码从离线静态升级为流式滚动更新,新内容冷启动用内容特征兜底;探索与利用——全按模型排序会造成反馈循环(没曝光的内容永远没数据),需要少量探索流量配合。
⚠️ 常见坑:把曝光日志直接当样本却忘了"未曝光 ≠ 不喜欢"。训练数据是模型自己挑选后的结果,位置偏差与选择偏差都会写进特征,第 6.1 节的"翻译业务语言"功夫在这里体现为偏差意识的自觉。
推荐是结构化特征的天下,文本与图像则是表示学习的领地——下一节看 XGBoost 在内容理解任务里的"接力跑"位置。