机器学习中类别变量的编码方法总结
机器学习中类别变量的编码方法总结
引言
在做结构化数据训练时,类别特征是非常常见的变量类型。多数模型与数值优化过程依赖数值输入,因此需要把类别转换为可计算的表示。常见路线包括标签编码、独热编码、目标编码以及树模型内置的类别处理等;各种方式在是否引入错误顺序假设、维度、对高基数特征的表达能力和是否易泄露标签上权衡不同。下文对常见做法做简要归纳。
1. 硬编码:Label Encoding
所谓硬编码,即直接对类别特征做数值映射:有多少个类别取值,就映射为多少个整数。这种方式简单直接,但仅在类别内部有明显顺序时较合适,例如学历:高中、本科、硕士、博士之间存在清晰的序关系。
从建模角度看,按字典序或出现顺序赋整数时,数值的增大并不对应真实的“强弱”关系,对拟合缺乏明确语义。对线性模型等会把输入当作有序刻度使用的算法,还可能引入人为偏置:两个本应平权的类别可能被编成 0 与 2,模型会误以为其间存在可比较的间隔。
sklearn 提供 LabelEncoder:
1
2
3
4
5
6
from sklearn import preprocessing
le = preprocessing.LabelEncoder()
le.fit(['undergraduate', 'master', 'PhD', 'Postdoc'])
le.transform(['undergraduate', 'master', 'PhD', 'Postdoc'])
# array([3, 2, 0, 1], dtype=int64)
2. 独热编码:One-hot Encoding
One-hot 是应用很广的类别编码方式:若某类别特征有 (m) 个取值,可转为 (m) 个二元特征,每一维对应一个类别。
当类别没有内在顺序、不宜用 Label Encoding 时,One-hot 能避免“错误顺序假设”带来的偏置。但若取值过多,容易带来维度灾难;对文本类特征若直接 One-hot,往往极为稀疏。经验上:类别无序且取值大约少于 5 个时可优先考虑 One-hot(个数并非硬性标准,需结合数据规模与模型)。
与树模型的关系:以 XGBoost、LightGBM 等为例,出于速度与过拟合控制,树深往往有限,对高基数类别的细分能力不足;此时在特征侧用目标编码等方式先压缩类别信息,有时能在相同树深下得到更好的拟合效果(可视作对浅树在高基数特征上的一种补偿)。
Pandas 与 sklearn 示例:
1
2
3
4
import pandas as pd
df = pd.DataFrame({'f1': ['A', 'B', 'C'], 'f2': ['Male', 'Female', 'Male']})
df = pd.get_dummies(df, columns=['f1', 'f2'])
1
2
3
4
5
6
7
8
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown='ignore')
X = [['Male', 1], ['Female', 3], ['Female', 2]]
enc.fit(X)
enc.transform([['Female', 1], ['Male', 4]]).toarray()
# array([[1., 0., 1., 0., 0.],
# [0., 1., 0., 0., 0.]])
3. 目标变量编码:Target Encoding
Target Encoding(目标编码)把每个类别映射成与该类别相关的目标变量统计量,常见的是各类别下标签的均值(回归)或正例比例(二分类)。编码后仍是一列(或少量列)数值特征,不会像 One-hot 那样随基数线性膨胀,因此在高基数类别上往往更省维度。下面先给出变体与注意,分步演算、平滑公式、代码示意及与 One-hot 的信息密度对比见 第 4 节。
监督性:TE 属于监督(或半监督中依赖标注子集)范畴——编码必须基于与建模目标一致的 (y)(回归值或类别标签);至少要有与「目标」同分布或可视为同一监督信号的标注,才能定义「各类别下对 (y) 的条件统计」。纯无监督聚类通常没有这样的 (y),一般不直接套用经典 TE;若用聚类得到的簇 id 等当作伪标签再做类似编码,已不属于标准 TE,且易与聚类过程纠缠,需单独论证泄露与稳定性。
K 折目标编码:基本思想仍是「用目标均值(或同类统计量)替换类别」,但在训练集上通过 K 折 / 折外(OOF) 估计各类统计量再写回本折样本,与下文 CV loop 同属减轻泄露、过拟合的做法。
Beta Target Encoding:思路来自 Kaggle Avito Demand Prediction 竞赛第 14 名 solution 的开源代码流传;相对「每类只编码一个标量(如均值)」的传统 Target Encoding,同一套构造里还可衍生方差等与目标在类内分布相关的量,特征列更多,但更依赖验证集与正则,避免噪声统计被模型放大。
基于目标构造编码的常见形式
除“各类别下目标均值”外,实务中还会用与目标相关的统计量做编码,例如(记号随任务定义正负样本或好坏类):
- 均值型:如 (\text{goods}/(\text{goods}+\text{bads})) 等比例;
- 证据权重(WOE):如 (\ln(\text{goods}/\text{bads})\times 100);
- 计数型:如 (\sum \text{Target});
- 差分型:如 (\text{goods}-\text{bads})。
具体定义需与业务标签约定一致。
标签泄漏、划分顺序与「看未来」
Target Encoding 显式使用标签 (y),因此除了「小样本统计噪声」带来的过拟合外,更容易在数据划分顺序和编码拟合范围上产生信息泄漏(俗称「偷看答案」「提前看到未来」)。它与单纯「模型参数记死训练集」不完全相同:泄漏往往让离线测试指标偏乐观,上线后泛化明显变差——更像评估被污染,而不只是训练 loss 走低。
训练/测试划分
- 随机划分本身通常不等价于「看未来」;但若业务本质是按时间预测(例如用历史行为预测下一期),却仍以随机切分评估,测试集中会混入「时间上更晚才合理可见」的模式,与真实部署不一致。
- 划分前泄漏:在尚未划分 train/valid/test 时,就对全表做依赖标签或全库分布的步骤(目标编码、基于 (y) 的特征选择、用全体数据估缺失填充/标准化参数等),验证集与测试集会间接携带本不应在拟合阶段获得的信息。
目标编码中的典型泄漏
- 全量拟合:用含验证/测试集在内的全体样本估计「类别 → 目标统计量」,属于标签泄漏,编码里已混入待预测目标的信息。
- 训练集内的行级泄漏:对训练样本第 (i) 行做编码时,若类内均值包含了第 (i) 行自身的 (y_i),训练集表现会偏乐观;验证/测试若仅用「训练集整体」映射则相对安全,但训练侧仍宜用 K 折 / OOF / 留一 等,使该行编码不依赖自己的标签。
何时容易发生
- 样本少、类别多,类内统计方差大;
- 时序数据却用随机切分,或编码用了「未来」样本;
- Pipeline 在 train 与 test 合并后的矩阵上
fit/fit_transform; - 同一实体(同人、同会话、同证件)的多条记录被拆到 train 与 test,按实体聚合的统计相当于跨集泄漏;
- 流水线中某一步使用全局而非训练折内的统计量。
规避要点(归纳)
- 先划分,后拟合:凡依赖 (y) 或应从训练分布学习的步骤,只在训练子集上
fit;对验证/测试仅transform。 - 目标编码:映射只在训练域估计;训练行用 K 折目标编码 / OOF / 有序编码(如 CatBoost 思路);平滑(Shrinkage)可减轻长尾类别噪声,但不能替代正确的划分与折内编码。
- 时序任务:按时间切分,编码统计仅限该评估点之前可见的历史。
- 实体级数据:按组(人/会话等)划分,避免同一实体同时出现在 train 与 test。
在同一份训练矩阵上误用全表统计的常见后果:若不用折外编码、直接用全表类内均值写回训练特征,训练集会异常好、测试集明显变差,既属泄漏,也常表现为过拟合。缓解的核心仍是:在训练数据内部做 CV loop(常见 4~5 折),验证折上的编码仅由当折训练子集算出的映射得到,再把各折结果拼回;映射不到的类别可用训练集整体目标均值等先验填充,并与上文的划分顺序、时序与分组策略一致。
进一步可做的正则化(可与折外编码组合)包括:
- 基于样本量的平滑(Smoothing):在「全局目标均值」与「该类条件均值」之间插值,小样本类别更偏向全局均值;
- 随机噪声:在编码值上施加可控扰动,降低对训练集统计量的死记硬背;
- 排序与扩展均值(expanding mean):按某种顺序逐批更新统计量,减轻特定划分带来的波动。
上述几类可单独使用,也可组合。
Target Encoding 的常见短板:未见类别在推理期缺乏可靠条件统计,易不稳或带来过拟合风险;空值若仅做填充再编码,评估结论可能对缺失机制不敏感;长尾类别样本极少时,类内均值/方差等估计方差大,同样易过拟合。实务上除 K 折与平滑 外,可结合先验回退(如全局均值)与交叉验证调强度。
适用场景与库实现备忘
适用场景:类别无序,且取值较多(可与「约 5 个」一类的经验分界对照使用,并结合交叉验证调参)。
自 scikit-learn 1.3 起,sklearn.preprocessing.TargetEncoder 提供内置目标编码:对每个类别使用收缩估计,在「该类条件目标均值」与全局目标均值之间混合,以缓解小样本类别不稳定;多分类时先做 one-vs-rest 再编码,输出维度可增至「特征数 × 类别数」。缺失值常被视为单独类别;未见类别多用全局目标均值编码。训练集上优先使用带内部交叉拟合的 fit_transform(X, y),与先 fit 再在同一批数据上 transform 的行为并不相同,后者更易泄露,文档一般不推荐单独依赖 fit+transform 于同一训练矩阵 来完成训练侧编码。测试集在训练阶段拟合完成后,仅用 transform。主要可调项包括类别列表、target_type(连续/二分类/多分类等,注意整数回归目标被误判为分类的情况)、smooth、cv 折数与是否打乱等。
官方 API 文档:sklearn.preprocessing.TargetEncoder
除上述外,category_encoders、CatBoost 等生态也提供成熟实现;手写多重循环易错,宜统一划分策略并尽量使用库内置的防泄露逻辑。
4. TE 详细示例:二分类编码、平滑与信息密度(对照 One-hot)
本节用两套示意:(一) 二分类 + 收缩平滑 + 代码;(二) 高基数场景下 One-hot 的稀疏高维 与 TE 单列稠密 的对比(常见于房价类教程中的 Neighborhood 思路)。均为教学数值,实务须配合 K 折 / OOF 防泄露。
4.1 二分类:用各类别正例比例编码(city / y)
下表为训练子集,city 为类别特征,y 为是否转化(0/1)。
| city | y |
|---|---|
| 北京 | 1 |
| 北京 | 0 |
| 上海 | 1 |
| 上海 | 1 |
| 广州 | 0 |
无平滑(纯类内均值):各类别下 (y) 的均值即该类「正例比例」:北京 ((1+0)/2=0.5),上海 (1.0),广州 (0.0)。目标编码把 city 替换为一列数值:北京→0.5、上海→1.0、广州→0.0。回归任务时同理,常把各类别下的 (y) 的算术平均 作为编码值。
接上表做平滑(Shrinkage toward global mean):先取训练集全局均值 (\bar{y}=\frac{3}{5}=0.6) 作为先验。设平滑强度 (m>0)(可理解为「等价伪样本数」或先验权重),对样本量为 (n_c)、类内均值为 (\hat{p}_c) 的类别 (c),常用形式为
[ \tilde{p}_c=\frac{n_c\,\hat{p}_c + m\,\bar{y}}{n_c+m} =\frac{n_c}{n_c+m}\hat{p}_c+\frac{m}{n_c+m}\bar{y}. ]
取 (m=2):北京 (n=2,\hat{p}=0.5),得 (\tilde{p}=\frac{1+1.2}{4}=0.55);上海 (\frac{2+1.2}{4}=0.8);广州仅 (n=1,\hat{p}=0),得 (\frac{0+1.2}{3}=0.4)。与无平滑相比,样本越少的类别越被拉向 (\bar{y}):广州从 (0.0) 明显抬高,上海从 (1.0) 下调,北京略向 (0.6) 靠拢。
为什么要平滑:类内均值是随机样本上的估计,(n_c) 很小时方差大,容易出现 (0) 或 (1) 这类极端编码,模型容易把噪声当成强信号,泛化变差。在 (\hat{p}_c) 与 (\bar{y}) 之间插值,等价于引入全局先验、压低小样本类别的波动,与经验贝叶斯或拉普拉斯平滑的思想一致;(m) 越大,整体越保守(更信全局)。实务中 (m) 常靠验证集或交叉验证与 K 折编码 一起调,并与泄露控制配合使用,而不是只依赖全表类内比例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
df = pd.DataFrame({
"city": ["北京", "北京", "上海", "上海", "广州"],
"y": [1, 0, 1, 1, 0],
})
enc = df.groupby("city")["y"].transform("mean") # 演示用:全表统计,易泄露
df["city_te"] = enc
# city_te: [0.5, 0.5, 1.0, 1.0, 0.0]
global_mean = df["y"].mean() # 0.6;实务中应用训练折内统计,勿用验证折
m = 2
stats = df.groupby("city")["y"].agg(["mean", "count"]).rename(
columns={"mean": "p_hat", "count": "n"}
)
def smooth_te(row):
return (row["n"] * row["p_hat"] + m * global_mean) / (row["n"] + m)
stats["p_smooth"] = stats.apply(smooth_te, axis=1)
df["city_te_smooth"] = df["city"].map(stats["p_smooth"])
# city_te_smooth: [0.55, 0.55, 0.8, 0.8, 0.4]
实务中不能像上面那样在同一批训练样本上「算完全表均值再写回」用于最终建模(见第 3 节泄露与 K 折);此处仅说明「编码长什么样」。推理时若出现训练未见城市,常用训练集全局 (\bar{y})(本例为 (3/5=0.6))回填;若采用与上式同一套平滑,也可将未见类视为 (n_c=0),则 (\tilde{p}_c=\bar{y})。
4.2 信息密度:One-hot 高维稀疏 vs TE 单列稠密(片区 / 房价示意)
设定:字段 Neighborhood(片区) 有 500 个取值;目标为房价(万美元)。One-hot 时每人一行对应 500 列,其中 499 个 0、1 个 1。TE 时每人一行只多 1 列 Neighborhood_te,值为该片区的(平滑后)训练集均价或与 (\log) 房价对齐的类内均值——与目标同量纲、强相关的标量。
下面用 3 个片区 A/B/C、5 套房 缩小演示(逻辑推广到 500 类即可)。
| 样本 | Neighborhood | SalePrice(万) |
|---|---|---|
| 1 | A | 30 |
| 2 | A | 34 |
| 3 | B | 80 |
| 4 | B | 88 |
| 5 | C | 45 |
One-hot(3 类 → 3 列;500 类则 500 列)
| 样本 | N_A | N_B | N_C |
|---|---|---|---|
| 1 | 1 | 0 | 0 |
| 2 | 1 | 0 | 0 |
| 3 | 0 | 1 | 0 |
| 4 | 0 | 1 | 0 |
| 5 | 0 | 0 | 1 |
每行仅 1 个 1,其余为 0;500 类时即「上千维稀疏」。线性模型需为每个片区估系数,维数随类别数线性膨胀。
TE(无平滑,仅示意类内均价):A 区 ((30+34)/2=32),B 区 (84),C 区 (45)。每人一行 1 列 Neighborhood_te:32, 32, 84, 84, 45。列数为 O(1),且该列直接携带「该片在训练里平均多贵」的稠密数值信号,线性模型可用 1 个系数吸收区位强弱,故称 信息密度更高。
平滑:若 C 区仅 1 套房,无平滑时 TE 易极端;用第 4.1 节同类收缩把编码拉向全局均价,仍只占 1 列,但更稳。
| 对比 | One-hot(以 500 类为例) | TE(1 列) |
|---|---|---|
| 列数 | 约 500 | 1 |
| 每行形态 | 1 个 1 + 大量 0 | 1 个实数 |
| 与目标关系 | 间接(指示属于哪一区) | 直接(类内目标均值 / 平滑后) |
5. 模型自动编码
在 LightGBM、CatBoost 等框架中,可将列声明为类别特征,由模型内部处理编码,无需事先手工 One-hot。例如 LightGBM(需 import lightgbm as lgb):
1
2
3
4
5
6
7
import lightgbm as lgb
lgb_train = lgb.Dataset(
train2[features],
train2['total_cost'],
categorical_feature=['sex'],
)
6. 小结对照表
| 方法 | 适用情况 |
|---|---|
| Label Encoding | 类别内部有序;对无序类别慎用于线性模型 |
| One-hot | 类别无序,取值较少(约 5 个以内可作经验参考) |
| Target Encoding | 类别无序、高基数更常见;须配合 CV、平滑等抑制泄露与过拟合 |
| 模型内类别特征 | LightGBM、CatBoost 等声明类别列,由模型内部编码 |
