这是「数据与特征工程」编码篇。前面把料洗净、切配、浓缩、挑好了,这一步是翻译菜谱——把「北京」「电子产品」这种文字类别,翻译成模型唯一能吃的东西:数字。
问题是,翻译方式不止一种,选错了轻则维度爆炸,重则数据泄漏、线上暴跌。
Table of contents
Open Table of contents
一句话理解
模型是个只认克数、不认字的机器人厨师。菜谱写「放一把盐」它看不懂,你得翻译成「盐 3 克」。
五种翻译法各有脾气:One-Hot 给每种料单开一栏(料少时清楚,料多时表格爆炸);Label 直接编号(省地方但会骗模型说有大小);Count / Frequency 用「这料出现多频繁」当编号;Target 最聪明也最危险——用「这料对应的历史成功率」当编号,一不小心就偷看了答案。
0. 为什么非编码不可
大多数模型的输入必须是数值。site_category = "news" 这种字符串,线性模型、SVM、神经网络根本没法算。类别特征分两种,决定了翻译策略:
- 名义型(无序):颜色、城市、行业——类别之间没有大小关系;
- 序数型(有序):学历、评分档位——类别之间有内在顺序。
1. 五种编码,一次看懂
用同一个例子:广告日志里的 城市 特征(北京出现多、成都出现少),标签是「是否点击」。
| 编码 | 「北京」变成 | 特点 | 主要坑 |
|---|---|---|---|
| One-Hot | [1,0,0] | 无序、正交,线性模型友好 | 高基数 → 维度爆炸 |
| Label | 0 | 省空间,树模型友好 | 给无序类别强加大小关系 |
| Count | 出现次数 5 | 保留热度信息、不增维 | 不同类别次数相同 → 撞码 |
| Frequency | 占比 0.42 | 同 Count 但归一化到 [0,1] | 同上,且需防数据泄漏 |
| Target | 历史 CTR 0.30 | 直接嵌入与标签的关系、不增维 | 数据泄漏 / 过拟合 |
1.1 One-Hot:给每种料单开一栏
K 个类别 → K 个 0/1 列。清晰、无序、线性模型友好。但在广告里是灾难:
用户ID 100万 + 广告ID 50万 + URL 20万 + 设备 5万 ≈ 175 万维
100 万样本 × 175 万维 × 4 字节 ≈ 7 TB 内存
所以高基数特征绝不直接 One-Hot。实战里常「只对高频 Top-N 类别 One-Hot,其余归为 other」,新类别用 handle_unknown='ignore' 全置 0。
1.2 Label:直接编号,但小心「假顺序」
["红","绿","蓝"] → [0,1,2]。省空间、树模型能很好处理(树只关心切分点,不在乎数值大小)。但对线性模型是陷阱——模型会以为「蓝(2) > 红(0)」,给无序类别硬造出大小关系。序数变量(学历高中/本科/硕士)才适合 Label。
1.3 Count / Frequency:用「多常见」当编号
- Count Encoding:用类别出现次数替代(北京 5 次 → 5)。热门类别可能更重要,且不增维。
- Frequency Encoding:用频率/占比(北京 0.42),归一化到 [0,1]。
| 计算 | 范围 | 何时用 | |
|---|---|---|---|
| Count | 出现次数 | [0, +∞) | 想保留绝对热度 |
| Frequency | 出现占比 | [0, 1] | 想标准化 / 对比占比 |
共同坑:两个无关类别若次数相同,会被编成同一个数(撞码);且频率要只在训练集统计,否则泄漏。罕见类别可用拉普拉斯平滑,冷启动给默认值。
2. 怎么选?看基数,也看模型
一张决策图记住:
- 有序 → Label / Ordinal(保住顺序信息);
- 无序 + 低基数(性别、设备类型)→ One-Hot;
- 无序 + 高基数(用户 ID、广告 ID)→ Target / Frequency / Count / Hashing / Embedding;
- 线性模型偏爱 One-Hot(正交),树模型能直接吃 Label / Count。
3. Target Encoding:最聪明,也最容易翻车
Target Encoding 用该类别对应的标签统计值(如历史 CTR)替代类别本身:
# 用用户历史点击率编码 user_id
user_ctr = train.groupby('user_id')['is_click'].mean()
train['user_id_encoded'] = train['user_id'].map(user_ctr)
一下把「user_12345」这种没法用的高基数 ID,变成了「这人 0.2 的点击率」这种极有预测力的数值,还不增维。广告里用它编码用户 ID、广告位、「小时 × 城市」组合,屡试不爽。但它有两个致命坑。
3.1 坑一:数据泄漏(偷看答案)
如果直接用全部数据(含验证/测试)算编码,等于让模型提前看了答案:
后果很典型:训练集 AUC 99%,线上 50%。就像用考试原题复习——分数虚高,真本事没有。
解法:K 折交叉编码。把训练集分成 K 折,每一折的编码值,只用其余 K−1 折的数据计算:
from sklearn.model_selection import KFold
def cv_target_encode(df, cat, target, n_splits=5, alpha=5):
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
encoded = pd.Series(index=df.index, dtype=float)
for trn, val in kf.split(df):
g = df.iloc[trn][target].mean()
agg = df.iloc[trn].groupby(cat)[target].agg(['mean', 'count'])
smooth = (agg['mean'] * agg['count'] + g * alpha) / (agg['count'] + alpha)
encoded.iloc[val] = df.iloc[val][cat].map(smooth).fillna(g)
return encoded
每个样本的编码都来自它没参与计算的那部分数据,从根上切断自我参照。
3.2 坑二:小样本过拟合 → 平滑收缩
如果「成都」只有 1 个用户且他点击了,直接编码就是 100%——显然不可靠。平滑公式把类别均值向全局均值「收缩」:
编码值 = (n · mean_类别 + α · mean_全局) / (n + α)
n大(样本多)→ 更信类别自己的均值;n小(样本少)→ 更信全局均值;α是「先验的虚拟样本量」,本质是贝叶斯收缩。
经验:数据越少、基数越高、分布越不均衡,α 就该越大(收缩更狠)。
一句话:Target Encoding = 把类别翻译成「历史成功率」。威力巨大,但必须配 K 折交叉 + 平滑两道保险,否则就是给模型喂兴奋剂——训练时爽,上线就现原形。
TL;DR — 三句话
- 模型只吃数字,类别必须编码;先分清无序(名义)还是有序(序数)。
- 按基数 + 模型选:有序用 Label;无序低基数用 One-Hot;无序高基数用 Target/Frequency/Hashing/Embedding。线性模型爱 One-Hot,树模型能吃 Label/Count。
- Target Encoding 最强也最险:用历史成功率编码高基数 ID,但必须配 K 折交叉编码(防泄漏)+ 平滑收缩(防小样本过拟合)。
系列位置
编码是「翻译菜谱」这一刀。至此理论链路已齐,下一篇用真实数据联考:
| # | 环节 | 文章 |
|---|---|---|
| 0 | 为什么特征决定上限 | 总览 |
| 1 | 特征怎么用/取/监控 | 系统化框架 |
| 2 | 洗菜择菜 | 数据预处理 |
| 3 | 切配调料 | 特征构建 |
| 4 | 吊高汤 | 特征提取与降维 |
| 5 | 只留好料 | 特征选择 |
| 6 | 翻译菜谱 | 本篇 |
| 7 | 联考实战 | Criteo CTR 实战 |
下一篇进入考场——Criteo CTR 特征工程实战:用公开广告点击日志,把清洗 → 变换 → 缩放 → 编码 → 选择串成一条离线/在线同口径的 Pipeline。