文章

机器学习中类别变量的编码方法总结

机器学习中类别变量的编码方法总结

机器学习中类别变量的编码方法总结


引言

在做结构化数据训练时,类别特征是非常常见的变量类型。多数模型与数值优化过程依赖数值输入,因此需要把类别转换为可计算的表示。常见路线包括标签编码、独热编码、目标编码以及树模型内置的类别处理等;各种方式在是否引入错误顺序假设维度对高基数特征的表达能力是否易泄露标签上权衡不同。下文对常见做法做简要归纳。


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 等为例,出于速度与过拟合控制,树深往往有限,对高基数类别的细分能力不足;此时在特征侧用目标编码等方式先压缩类别信息,有时能在相同树深下得到更好的拟合效果(可视作对浅树在高基数特征上的一种补偿)。

Pandassklearn 示例:

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,按实体聚合的统计相当于跨集泄漏;
  • 流水线中某一步使用全局而非训练折内的统计量。

规避要点(归纳)

  1. 先划分,后拟合:凡依赖 (y) 或应从训练分布学习的步骤,只在训练子集fit;对验证/测试仅 transform
  2. 目标编码:映射只在训练域估计;训练行用 K 折目标编码 / OOF / 有序编码(如 CatBoost 思路)平滑(Shrinkage)可减轻长尾类别噪声,但不能替代正确的划分与折内编码。
  3. 时序任务:按时间切分,编码统计仅限该评估点之前可见的历史。
  4. 实体级数据:按组(人/会话等)划分,避免同一实体同时出现在 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(连续/二分类/多分类等,注意整数回归目标被误判为分类的情况)、smoothcv 折数与是否打乱等。

官方 API 文档sklearn.preprocessing.TargetEncoder

除上述外,category_encodersCatBoost 等生态也提供成熟实现;手写多重循环易错,宜统一划分策略并尽量使用库内置的防泄露逻辑。


4. TE 详细示例:二分类编码、平滑与信息密度(对照 One-hot)

本节用两套示意:(一) 二分类 + 收缩平滑 + 代码;(二) 高基数场景下 One-hot 的稀疏高维TE 单列稠密 的对比(常见于房价类教程中的 Neighborhood 思路)。均为教学数值,实务须配合 K 折 / OOF 防泄露。

4.1 二分类:用各类别正例比例编码(city / y

下表为训练子集,city 为类别特征,y 为是否转化(0/1)。

cityy
北京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 类即可)。

样本NeighborhoodSalePrice(万)
1A30
2A34
3B80
4B88
5C45

One-hot(3 类 → 3 列;500 类则 500 列)

样本N_AN_BN_C
1100
2100
3010
4010
5001

每行仅 1 个 1,其余为 0;500 类时即「上千维稀疏」。线性模型需为每个片区估系数,维数随类别数线性膨胀

TE(无平滑,仅示意类内均价):A 区 ((30+34)/2=32),B 区 (84),C 区 (45)。每人一行 1 列 Neighborhood_te32, 32, 84, 84, 45。列数为 O(1),且该列直接携带「该片在训练里平均多贵」的稠密数值信号,线性模型可用 1 个系数吸收区位强弱,故称 信息密度更高。

平滑:若 C 区仅 1 套房,无平滑时 TE 易极端;用第 4.1 节同类收缩把编码拉向全局均价,仍只占 1 列,但更稳。

对比One-hot(以 500 类为例)TE(1 列)
列数约 5001
每行形态1 个 1 + 大量 01 个实数
与目标关系间接(指示属于哪一区)直接(类内目标均值 / 平滑后)

5. 模型自动编码

LightGBMCatBoost 等框架中,可将列声明为类别特征,由模型内部处理编码,无需事先手工 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、平滑等抑制泄露与过拟合
模型内类别特征LightGBMCatBoost 等声明类别列,由模型内部编码

本文由作者按照 CC BY 4.0 进行授权