用数据证明直觉:CTR 特征工程的完整实战复盘

我在 Criteo 广告点击数据上做了一组完整的特征工程实验。结果有些出乎意料:原始的 39 个特征,每一个单独拿出来做 IV(Information Value)评估,全部被判定为 “useless”——预测力不足。但经过系统的特征构造之后,AUC 提升了 3.42%,而且模型最终学到的 Top 9 重要特征全部是新构造出来的。

这篇文章我想记录的是一个完整的思考过程:怎么从一堆看起来”没用”的原始数据里,用数据分析工具一步步把有价值的信号挖出来。

起点:所有特征都”没用”?

先说数据背景。Criteo 是广告行业最常用的公开 CTR 数据集,包含 13 个数值特征(I1-I13)和 26 个类别特征(C1-C26),全部匿名化处理。我做了 RTB 场景的降采样处理,最终 CTR 约 5%,100 万条样本。

拿到数据第一件事,我用 IV/WoE 对每个特征做了预测力评估。IV 是金融风控领域常用的特征筛选指标,规则很简单:

IV < 0.02:  useless(不值得入模)
0.02-0.1:   weak
0.1-0.3:    medium
0.3-0.5:    strong
> 0.5:      suspicious(可能数据泄露)

结果是这样的:

排名特征IV评级
1I90.0165useless
2I50.0161useless
3C120.0157useless
4I10.0156useless
5I20.0140useless

全军覆没。没有一个特征的 IV 超过 0.02 的 weak 门槛。

但这并不意味着这些特征真的没用。用同样的数据训练 XGBoost,AUC 能到 0.5892——远高于随机猜的 0.5。树模型靠的是特征组合而非单个特征。IV 衡量的是单一特征的线性区分能力,而现实中有预测力的往往是特征之间的交互。

不过 IV 作为快速筛选工具仍然有价值。我拿 IV 排名和 SHAP 重要性排名、XGBoost gain 排名做了交叉验证:

特征IV 排名SHAP 排名XGBoost gain 排名
I9113
I5232
I1421
I2544

三种方法的 Top 4 完全重合,只是内部排序有差异。IV 的优势在于速度——毫秒级完成,SHAP 要秒级,适合大规模特征的第一轮初筛。

让数据告诉你该构造什么特征

传统做法是凭经验手工构造交叉特征,比如”时段 x 设备”、“广告主 x 广告位”。这种做法有两个问题:一是依赖领域经验,新人很难复制;二是容易遗漏真正有价值的组合。

我换了个思路:用 SHAP interaction values 直接量化特征之间的交互强度,看看哪些组合真的有信号。对 XGBoost baseline 模型做 interaction 分析后,得到这样的结果:

特征对Mean Interaction
I5 x I90.005987
I1 x I90.005486
I2 x I50.005060
I2 x I90.004367
I1 x I50.004266
I1 x I20.004112

一个清晰的模式浮出来了:I1、I2、I5、I9 四个特征之间的两两交互值都远高于其他组合。如果这是真实广告数据,它们可能分别对应”广告主预算”、“用户活跃度”、“时段热度”、“广告位价值”之类的业务维度——匿名数据里看不到语义,但交互模式能帮我们猜到它们之间存在有意义的关联。

这就给出了明确的特征构造方向:优先构造这个四元组内部的两两交叉。

构造特征:四类方法,各有各的道理

基于 SHAP interaction 的分析结果,我构造了四类新特征,总共 20 个。

交叉特征:把交互显式化

取 SHAP interaction 排名前 5 的特征对,直接做乘积:

新特征来源IV
I5_x_I9I5 * I90.0238
I1_x_I9I1 * I90.0228
I1_x_I5I1 * I50.0224
I2_x_I9I2 * I90.0216
I2_x_I5I2 * I50.0207

注意看 IV 值——交叉特征的 IV 全部 > 0.02,原始特征全部 < 0.02。单独看每个特征都”没用”,但组合起来就跨过了 weak 的门槛。FM(Factorization Machine)的核心思想就是这个:真正有预测力的不是单个特征,而是特征之间的二阶交互。

# SHAP interaction 指导下的交叉特征构造
top_interactions = [('I5', 'I9'), ('I1', 'I9'), ('I1', 'I5'),
                    ('I2', 'I9'), ('I2', 'I5')]

for f1, f2 in top_interactions:
    df[f'{f1}_x_{f2}'] = df[f1] * df[f2]

时间特征:sin/cos 完胜二值标记

实验数据里有 hour(0-23),我用了两种编码方式:

新特征编码方式IV
hour_coscos(2π · hour/24)0.0262
hour_sinsin(2π · hour/24)0.0129
is_peak高峰时段二值标记0.0000
is_night夜间二值标记0.0000

sin/cos 周期编码的 IV 远高于二值标记。hour_cos 的 IV 达到 0.0262,是所有新特征中最高的。而 is_peakis_night 的 IV 几乎为零。

原因很直接:sin/cos 保留了时间的连续性和周期性(23点和0点在数值上很远,但在 cos 编码下很近),而二值标记把连续信号硬压成了 0/1,丢失了绝大部分信息。

# 周期编码:保留时间的连续性
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)

这个结论在广告场景中很实用。生产系统里通常把 hour 当类别特征做 Embedding,本质上也是在学一个连续表示。但如果你用的是树模型或者线性模型,sin/cos 编码是最简单有效的做法。

统计特征和分箱特征

统计特征是对 13 个数值特征做行级聚合(均值、标准差、最大值等)。效果一般,IV 大多在 0.01 左右。分箱特征是把数值特征离散化成分位数桶,帮助树模型找到更精确的分裂点。

这两类特征的价值不如交叉特征和时间特征,但胜在稳定——几乎在任何数据集上都能提供一些增量信息。

筛选和验证:特征工程的闭环

构造了 20 个新特征后,不是全部丢进模型就完事了。我用了一个简单的 IV 筛选 pipeline:

  1. 对每个新特征计算 IV
  2. IV >= 0.01 的保留
  3. 被筛掉的:is_peakis_nightnum_nonzeronum_max_rationum_min —— 5 个
  4. 保留 15 个新特征

然后做端到端对比:

实验特征数AUCLogLoss
Baseline(原始特征)230.56720.1923
Enhanced(原始 + 新特征)380.58660.1916
仅新特征150.58620.1916

几个值得注意的点:

AUC 提升了 +0.0194(+3.42%)。在 CTR 预估领域,这个幅度不算小。线上系统里 0.01 的 AUC 提升就值得上一次 AB 实验。

仅用 15 个构造特征就几乎等同于全部 38 个特征的效果(0.5862 vs 0.5866)。新特征已经捕获了原始特征的大部分信息,甚至更紧凑。

但最让我意外的是增强模型中特征重要性的排名:

排名特征XGBoost Gain来源
1hour_cos62.85[新] 时间
2I5_x_I936.51[新] 交叉
3I9_bin1033.87[新] 分箱
4I1_x_I930.07[新] 交叉
5hour_sin26.31[新] 时间
10I117.27原始

Top 9 全是新构造的特征。原始特征从第 10 名才开始出现。模型很诚实:显式构造的交叉、分箱、时间特征比原始特征提供了更直接的预测信号。

回到真实世界:生产环境的特征工程长什么样

实验做完,我拿这套流程和自己负责的 RTB 生产系统做了对比。教科书和生产之间的差距,比我预想的大得多。

手动交叉 vs FM 自动交叉

实验里我手动构造 I5 * I9 这种乘积特征,在树模型上效果不错。但在生产系统里,FM 层已经在做这件事了——而且是全量的:

FM: 0.5 * [(Σ vi·xi)² - Σ(vi·xi)²]

FM 对所有特征对做二阶交互,复杂度 O(kn),不需要你挑选哪些特征对值得交叉。所以在用了 FM 的生产系统里,手动构造交叉特征的价值被大幅压缩。

Embedding vs Label Encoding

实验中一个让我印象深刻的发现:26 个类别特征(C1-C26)做 Label Encoding 后喂给 XGBoost,AUC 只有 0.5061——跟随机猜差不多。

原因是 hash 过的类别值对树模型来说就是无序整数。“C3=42” 和 “C3=99137”,树只知道 42 < 99137,但这个大小关系没有任何语义。

生产系统用 Embedding 解决这个问题:每个类别值映射到一个可学习的向量空间,语义相近的广告主、创意会有相近的向量表示。深度 CTR 模型超越树模型的核心不是网络更深,而是表示能力更强。

方法本次实验生产系统
交叉特征手动乘积 (I5 * I9)FM 自动全量二阶交叉
时间编码sin/coshour 作为类别 Embedding
统计特征行级 sum/max/std历史 CTR/曝光/点击统计
分箱quantile binningGo 端 BucketEncoder
特征选择IV + SHAP业务经验 + 线上 AB

生产中特征工程的真正战场

对比下来,我发现生产环境中特征工程的重点根本不在手动交叉这些事上。FM 已经覆盖了大部分,真正花精力的地方是另外几件事。

历史统计特征的时间窗口设计。生产系统的 StatisticsProcessor 提供了过去 7 天的 CTR、过去 30 天的曝光数、上一次点击的时间间隔这类特征。Criteo 数据集里完全没有这些,但在真实广告系统中这才是最有预测力的信号。

Training-Serving 一致性。生产系统中特征处理分两端:Go 端的 FeatureProcessor 负责 Serving 时的实时特征提取,Python 端的 feature_processor.py 负责训练时的批处理。两端对同一个特征的处理逻辑必须完全一致——分桶边界、缺失值填充、归一化参数,差一点点就会引入 Training-Serving Skew,直接吃掉离线 AUC 的提升。这个问题不性感,但杀伤力巨大。

新特征的线上验证。离线 AUC 提升不等于线上收入提升。一个新特征从提出到上线可能要跑两周的 AB 测试。

回头看

回顾整个实验,有几件事我自己印象比较深。

IV/WoE 做特征初筛很快。毫秒级完成,结果和 SHAP、XGBoost gain 高度一致。特征多的时候先用 IV 过一遍,省掉很多时间。

SHAP interaction 比拍脑袋靠谱。实验里它指出的 I1/I2/I5/I9 四元组,构造出来的交叉特征 IV 全部突破了 useless 的门槛。如果没有这个分析,我大概率不会刚好猜中这几个组合。

时间特征别用二值标记。cos 编码的 IV 是 0.0262,is_peak 和 is_night 是 0.0000。差距太明显了。

特征工程对树模型价值大,对深度模型价值小。本次实验在 XGBoost 上拿到 +3.42% AUC,但换成 DeepFM(FM 层自动做交叉),增量会小得多。用什么模型,决定了你的特征工程应该花在哪。

最后一个:特征不是越多越好。消融实验里,用 SHAP 排名前 10 的特征训练出来的模型 AUC 反而比全部 85 个特征高。冗余特征是噪声。