Skip to content
Charles Shao
Go back

特征编码:把文字菜谱翻译成秤上的克数

views

这是「数据与特征工程」编码篇。前面把料洗净切配浓缩挑好了,这一步是翻译菜谱——把「北京」「电子产品」这种文字类别,翻译成模型唯一能吃的东西:数字

问题是,翻译方式不止一种,选错了轻则维度爆炸,重则数据泄漏、线上暴跌。

Table of contents

Open Table of contents

一句话理解

模型是个只认克数、不认字的机器人厨师。菜谱写「放一把盐」它看不懂,你得翻译成「盐 3 克」。

五种翻译法各有脾气:One-Hot 给每种料单开一栏(料少时清楚,料多时表格爆炸);Label 直接编号(省地方但会骗模型说有大小);Count / Frequency 用「这料出现多频繁」当编号;Target 最聪明也最危险——用「这料对应的历史成功率」当编号,一不小心就偷看了答案


0. 为什么非编码不可

大多数模型的输入必须是数值。site_category = "news" 这种字符串,线性模型、SVM、神经网络根本没法算。类别特征分两种,决定了翻译策略:


1. 五种编码,一次看懂

用同一个例子:广告日志里的 城市 特征(北京出现多、成都出现少),标签是「是否点击」。

五种编码对照:同一个「城市」类别,One-Hot 展开成多列 0/1,Label 编成整数 0/1/2(暗示大小),Count 用出现次数,Frequency 用出现频率[0,1],Target 用该类别的历史点击率——维度、是否暗示顺序、是否用到标签各不相同

编码「北京」变成特点主要坑
One-Hot[1,0,0]无序、正交,线性模型友好高基数 → 维度爆炸
Label0省空间,树模型友好给无序类别强加大小关系
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出现次数[0, +∞)想保留绝对热度
Frequency出现占比[0, 1]想标准化 / 对比占比

共同坑:两个无关类别若次数相同,会被编成同一个数(撞码);且频率要只在训练集统计,否则泄漏。罕见类别可用拉普拉斯平滑,冷启动给默认值。


2. 怎么选?看基数,也看模型

特征编码决策图:先看类别是否有序——有序用 Label/Ordinal;无序再看基数——低基数用 One-Hot,高基数改用 Target / Frequency / Count / Hashing / Embedding;线性模型偏爱 One-Hot、树模型能吃 Label

一张决策图记住:


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 坑一:数据泄漏(偷看答案)

如果直接用全部数据(含验证/测试)算编码,等于让模型提前看了答案

Target Encoding 数据泄漏与 K 折交叉编码:左边直接用全量数据算编码再套回自己,等于考前偷看答案,训练虚高、上线暴跌;右边把数据分 K 折,每折的编码只用「其余折」计算,编码永远来自模型没见过的数据,避免自我参照

后果很典型:训练集 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 + α)

经验:数据越少、基数越高、分布越不均衡,α 就该越大(收缩更狠)。

一句话:Target Encoding = 把类别翻译成「历史成功率」。威力巨大,但必须配 K 折交叉 + 平滑两道保险,否则就是给模型喂兴奋剂——训练时爽,上线就现原形。


TL;DR — 三句话

  1. 模型只吃数字,类别必须编码;先分清无序(名义)还是有序(序数)。
  2. 按基数 + 模型选:有序用 Label;无序低基数用 One-Hot;无序高基数用 Target/Frequency/Hashing/Embedding。线性模型爱 One-Hot,树模型能吃 Label/Count。
  3. Target Encoding 最强也最险:用历史成功率编码高基数 ID,但必须配 K 折交叉编码(防泄漏)+ 平滑收缩(防小样本过拟合)。

系列位置

编码是「翻译菜谱」这一刀。至此理论链路已齐,下一篇用真实数据联考:

#环节文章
0为什么特征决定上限总览
1特征怎么用/取/监控系统化框架
2洗菜择菜数据预处理
3切配调料特征构建
4吊高汤特征提取与降维
5只留好料特征选择
6翻译菜谱本篇
7联考实战Criteo CTR 实战

下一篇进入考场——Criteo CTR 特征工程实战:用公开广告点击日志,把清洗 → 变换 → 缩放 → 编码 → 选择串成一条离线/在线同口径的 Pipeline。


views
Share this post on:

Previous Post
广告多维报表与漏斗分析实战
Next Post
特征选择:只留好料——过滤、包装、嵌入三大流派