🥇用数据证明直觉:CTR 特征工程的完整实战复盘
type
Post
status
Published
date
Feb 27, 2026
slug
summary
tags
category
技术分享
icon
password
Status
我在 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 | 评级 |
1 | I9 | 0.0165 | useless |
2 | I5 | 0.0161 | useless |
3 | C12 | 0.0157 | useless |
4 | I1 | 0.0156 | useless |
5 | I2 | 0.0140 | useless |
全军覆没。没有一个特征的 IV 超过 0.02 的 weak 门槛。
但这并不意味着这些特征真的没用。用同样的数据训练 XGBoost,AUC 能到 0.5892——远高于随机猜的 0.5。树模型靠的是特征组合而非单个特征。IV 衡量的是单一特征的线性区分能力,而现实中有预测力的往往是特征之间的交互。
不过 IV 作为快速筛选工具仍然有价值。我拿 IV 排名和 SHAP 重要性排名、XGBoost gain 排名做了交叉验证:
特征 | IV 排名 | SHAP 排名 | XGBoost gain 排名 |
I9 | 1 | 1 | 3 |
I5 | 2 | 3 | 2 |
I1 | 4 | 2 | 1 |
I2 | 5 | 4 | 4 |
三种方法的 Top 4 完全重合,只是内部排序有差异。IV 的优势在于速度——毫秒级完成,SHAP 要秒级,适合大规模特征的第一轮初筛。
让数据告诉你该构造什么特征
传统做法是凭经验手工构造交叉特征,比如"时段 x 设备"、"广告主 x 广告位"。这种做法有两个问题:一是依赖领域经验,新人很难复制;二是容易遗漏真正有价值的组合。
我换了个思路:用 SHAP interaction values 直接量化特征之间的交互强度,看看哪些组合真的有信号。对 XGBoost baseline 模型做 interaction 分析后,得到这样的结果:
特征对 | Mean Interaction |
I5 x I9 | 0.005987 |
I1 x I9 | 0.005486 |
I2 x I5 | 0.005060 |
I2 x I9 | 0.004367 |
I1 x I5 | 0.004266 |
I1 x I2 | 0.004112 |
一个清晰的模式浮出来了:I1、I2、I5、I9 四个特征之间的两两交互值都远高于其他组合。如果这是真实广告数据,它们可能分别对应"广告主预算"、"用户活跃度"、"时段热度"、"广告位价值"之类的业务维度——匿名数据里看不到语义,但交互模式能帮我们猜到它们之间存在有意义的关联。
这就给出了明确的特征构造方向:优先构造这个四元组内部的两两交叉。
构造特征:四类方法,各有各的道理
基于 SHAP interaction 的分析结果,我构造了四类新特征,总共 20 个。
交叉特征:把交互显式化
取 SHAP interaction 排名前 5 的特征对,直接做乘积:
新特征 | 来源 | IV |
I5_x_I9 | I5 * I9 | 0.0238 |
I1_x_I9 | I1 * I9 | 0.0228 |
I1_x_I5 | I1 * I5 | 0.0224 |
I2_x_I9 | I2 * I9 | 0.0216 |
I2_x_I5 | I2 * I5 | 0.0207 |
注意看 IV 值——交叉特征的 IV 全部 > 0.02,原始特征全部 < 0.02。单独看每个特征都"没用",但组合起来就跨过了 weak 的门槛。FM(Factorization Machine)的核心思想就是这个:真正有预测力的不是单个特征,而是特征之间的二阶交互。
时间特征:sin/cos 完胜二值标记
实验数据里有 hour(0-23),我用了两种编码方式:
新特征 | 编码方式 | IV |
hour_cos | cos(2π · hour/24) | 0.0262 |
hour_sin | sin(2π · hour/24) | 0.0129 |
is_peak | 高峰时段二值标记 | 0.0000 |
is_night | 夜间二值标记 | 0.0000 |
sin/cos 周期编码的 IV 远高于二值标记。
hour_cos 的 IV 达到 0.0262,是所有新特征中最高的。而 is_peak 和 is_night 的 IV 几乎为零。原因很直接:sin/cos 保留了时间的连续性和周期性(23点和0点在数值上很远,但在 cos 编码下很近),而二值标记把连续信号硬压成了 0/1,丢失了绝大部分信息。
这个结论在广告场景中很实用。生产系统里通常把 hour 当类别特征做 Embedding,本质上也是在学一个连续表示。但如果你用的是树模型或者线性模型,sin/cos 编码是最简单有效的做法。
统计特征和分箱特征
统计特征是对 13 个数值特征做行级聚合(均值、标准差、最大值等)。效果一般,IV 大多在 0.01 左右。分箱特征是把数值特征离散化成分位数桶,帮助树模型找到更精确的分裂点。
这两类特征的价值不如交叉特征和时间特征,但胜在稳定——几乎在任何数据集上都能提供一些增量信息。
筛选和验证:特征工程的闭环
构造了 20 个新特征后,不是全部丢进模型就完事了。我用了一个简单的 IV 筛选 pipeline:
- 对每个新特征计算 IV
- IV >= 0.01 的保留
- 被筛掉的:
is_peak、is_night、num_nonzero、num_max_ratio、num_min—— 5 个
- 保留 15 个新特征
然后做端到端对比:
实验 | 特征数 | AUC | LogLoss |
Baseline(原始特征) | 23 | 0.5672 | 0.1923 |
Enhanced(原始 + 新特征) | 38 | 0.5866 | 0.1916 |
仅新特征 | 15 | 0.5862 | 0.1916 |
几个值得注意的点:
AUC 提升了 +0.0194(+3.42%)。在 CTR 预估领域,这个幅度不算小。线上系统里 0.01 的 AUC 提升就值得上一次 AB 实验。
仅用 15 个构造特征就几乎等同于全部 38 个特征的效果(0.5862 vs 0.5866)。新特征已经捕获了原始特征的大部分信息,甚至更紧凑。
但最让我意外的是增强模型中特征重要性的排名:
排名 | 特征 | XGBoost Gain | 来源 |
1 | hour_cos | 62.85 | [新] 时间 |
2 | I5_x_I9 | 36.51 | [新] 交叉 |
3 | I9_bin10 | 33.87 | [新] 分箱 |
4 | I1_x_I9 | 30.07 | [新] 交叉 |
5 | hour_sin | 26.31 | [新] 时间 |
... | ... | ... | ... |
10 | I1 | 17.27 | 原始 |
Top 9 全是新构造的特征。原始特征从第 10 名才开始出现。模型很诚实:显式构造的交叉、分箱、时间特征比原始特征提供了更直接的预测信号。
回到真实世界:生产环境的特征工程长什么样
实验做完,我拿这套流程和自己负责的 RTB 生产系统做了对比。教科书和生产之间的差距,比我预想的大得多。
手动交叉 vs FM 自动交叉
实验里我手动构造
I5 * I9 这种乘积特征,在树模型上效果不错。但在生产系统里,FM 层已经在做这件事了——而且是全量的: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/cos | hour 作为类别 Embedding |
统计特征 | 行级 sum/max/std | 历史 CTR/曝光/点击统计 |
分箱 | quantile binning | Go 端 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 个特征高。冗余特征是噪声。
