类别特征编码:从独热到目标编码与留一法

FreeGuideOnline 最新 2026-06-14

类别特征编码方法:从独热到目标编码与留一法

在机器学习的特征工程中,类别特征的处理直接影响模型的表现。本教程从初学者的视角,由浅入深讲解三大核心编码技术:独热编码、目标编码与留一法目标编码,帮助你在真实项目中做出合理选择。

什么是类别特征与编码?

类别特征(Categorical Features)是指取值为有限个离散项的特征,例如“城市”、“性别”、“商品品类”。由于大部分机器学习算法只能处理数值型输入,我们需要将这些文本标签转换为数字,这个过程称为类别特征编码

编码方式的优劣会带来以下几方面的影响:

  • 模型的学习能力与收敛速度
  • 过拟合风险
  • 高基数(类别数很多)特征的处理难度

下面我们将逐步拆解常用方法。

1. 基础编码:独热与标签

1.1 标签编码(Label Encoding)

标签编码直接将每个类别映射为从 0 开始的整数。例如 ['北京','上海','广州'] 变为 [0,1,2]

实现

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['city_encoded'] = le.fit_transform(df['city'])

致命缺陷:整数天然带有顺序关系(0<1<2),但类别之间通常是无序的。线性模型、神经网络等会错误地认为“上海”比“北京”大,而“广州”又更大,这种虚假的顺序关系会严重干扰模型学习。仅当类别真的存在明确顺序(例如“低、中、高”)时,才可谨慎使用。

1.2 独热编码(One-Hot Encoding)

独热编码为每个类别创建一个新的二进制列(0/1),样本只在对应的列上取 1,其余为 0。

示例

城市 城市_北京 城市_上海 城市_广州
北京 1 0 0
上海 0 1 0
广州 0 0 1

优点

  • 完全消除顺序关系,所有类别“平等”对待
  • 适合逻辑回归、线性回归、神经网络等对数值大小敏感的模型

缺点

  • 类别基数(Cardinality)很高时,特征维度爆炸(例如“用户ID”有10万取值,则产生10万列)
  • 产生稀疏矩阵,对内存和计算效率不友好
  • 树模型(如随机森林、XGBoost)处理大量稀疏特征时,分裂效率较低

实现

import pandas as pd
one_hot = pd.get_dummies(df['city'], prefix='city')
# 或使用 scikit-learn
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False)
encoded = ohe.fit_transform(df[['city']])

大多数情况下,独热编码是安全、无信息泄露的基线方法。但当类别数超过几百个时,就需要更先进的编码策略。

2. 目标编码:用目标信息编码类别

目标编码(Target Encoding)的核心思想是:用每个类别对应的目标变量平均值(或其它统计量)替换类别标签。这种方法能够将高基数类别压缩为一列,同时直接融入目标信息,使模型更有效地利用类别对目标的影响。

2.1 基本原理

以二分类任务(目标 y 为 0/1)为例,对于类别“北京”,我们计算该类别下所有样本的 y 均值:

target_encode(北京) = mean(y 值,其中城市=北京)

回归问题则计算平均目标值。对于多分类,可对每一类设置一个二元目标进行编码。

直观示例

城市 购买率(均值编码)
北京 0.32
上海 0.45
广州 0.28

新样本中的“北京”直接被替换为 0.32。这种方法天然处理高基数,且编码值本身包含了对目标的预测能力。

2.2 过拟合灾难与数据泄露

目标编码的最大问题是极易造成过拟合。如果直接用全体训练数据计算每个类别的全局均值,那么:

  • 稀有类别因为样本极少,其均值极端(全部为0或1),编码值高度依赖个别样本,泛化能力极差。
  • 对于出现过仅一次或少数几次的类别,模型几乎是在“背诵”训练集。
  • 数据泄露:使用所有训练样本计算编码,相当于把目标信息提前“泄露”给了特征,尤其在使用交叉验证评估时,若编码阶段就看到了验证集目标,会造成严重偏差。

因此,必须引入正则化或特殊处理来缓解过拟合。

2.3 简单平滑正则化

一个常见做法是将类别均值与全局均值进行加权混合:

encoded = (n * category_mean + m * global_mean) / (n + m)

其中 n 为该类别样本数,m 为平滑因子(控制全局均值的权重)。当样本数很小时,更相信全局均值;当样本足够多时,更相信类别自身均值。

许多库(如 category_encoders)提供了内置的参数来实现这种平滑。

3. 留一法目标编码(Leave-One-Out Target Encoding)

3.1 为什么需要留一法?

简单目标编码中,一个样本的编码值包含了它自己的目标信息,这导致模型在训练时直接“偷看”到了自己的答案,特征与目标之间产生虚假的强相关性。留一法(LOO)通过在编码时排除当前样本自身来彻底解决这个问题。

3.2 工作原理

对于训练集中的每一条样本 i,其类别 k 的留一编码计算方式为:

loo_encoded(i) = mean(y_j),其中 j ≠ i 且 类别(j) = k

也就是计算该类别在所有其他同类样本上的目标均值。对于只有 1 个样本的类别,可以回退到全局均值或其它先验。

计算实例: 假设类别“A”有3个样本,目标值分别为 1, 0, 1。

  • 对第一个样本(目标=1),LOO 编码 = mean([0,1]) = 0.5
  • 对第二个样本(目标=0),LOO 编码 = mean([1,1]) = 1.0
  • 对第三个样本(目标=1),LOO 编码 = mean([1,0]) = 0.5

可以看到,每个样本的编码值完全不包含自身的目标信息,从根本上阻断了数据泄露。在预测新数据(测试集)时,我们使用整个训练集计算的类别均值作为编码值,不适用留一法(因为测试集没有标签)。

3.3 实现方法

你可以手动实现LOO编码,但更推荐使用专业特征工程库 category_encoders

import category_encoders as ce

# LeaveOneOutEncoder 自动完成训练集中的留一编码以及测试集的全局编码
encoder = ce.LeaveOneOutEncoder(cols=['city'])
X_train_encoded = encoder.fit_transform(X_train, y_train)
X_test_encoded = encoder.transform(X_test)

性能考量:LOO编码需要在训练阶段计算大量条件均值,O(n) 复杂度,在现代计算机上处理数十万样本通常可接受。若数据量极大,可以采用基于交叉验证的替代方案。

3.4 留一法的优缺点

优点

  • 完全避免单样本级别的目标泄露
  • 极大程度抑制过拟合,编码更加稳健
  • 对稀有类别友好(其编码接近全局均值)

缺点

  • 训练时的计算量比普通目标编码稍大
  • 测试集编码使用全体训练集均值,仍可能存在轻微的数据分布偏移(但不属于泄露)
  • 其实质上是训练过程中的一种特殊正则化,在极端情况下可能略微损失编码的预测力

4. 进阶:交叉验证目标编码

留一法可以看作是 N 折交叉验证的特例(N=样本数)。实践中,常使用 K-Fold 目标编码

  1. 将训练数据分成 K 折
  2. 对每一折样本,使用其余 K-1 折数据计算类别均值进行编码
  3. 对测试集,使用全部训练数据计算的均值编码

这种方式既避免了泄露,又通过分折减少了计算开销(相比LOO),并且可以通过多次不同分折求均值来增加稳定性。category_encoders 中的 TargetEncoder 通常就支持这种交叉验证模式。

encoder = ce.TargetEncoder(cols=['city'], smoothing=10)  # 还有平滑参数

5. 方法对比与选择指南

编码方法 适用场景 注意事项
独热编码 低基数(类别<10~20),线性模型、神经网络 高基数时维度灾难,树模型可用但效率低
标签编码 有序类别,或树模型能天然处理顺序影响(GBDT 可学习最优分裂点) 无序类别会误导线性模型,慎用
目标编码(正则化) 高基数类别,任何模型,尤其需要强预测信号时 必须配合平滑或交叉验证,防止泄露
留一法目标编码 小数据集、严格避免泄露、稀有类别多的情况 是训练时的无泄露编码器,泛化性强,作为首选目标编码方案

实战建议

  • 树模型(XGBoost, LightGBM, CatBoost):CatBoost 自带基于目标统计的编码并采用留一法或类似策略,可直接输入原始类别。XGBoost 可使用目标编码或直接尝试标签编码,因为树会学习分割点,标签编码的顺序影响可通过大量分裂消除,但通常推荐使用正则化目标编码获得更好效果。
  • 线性模型 / 神经网络:严禁使用标签编码,优先使用独热编码(低基数时)或目标编码(高基数时)。留一法目标编码是可靠选择。
  • 时间序列数据:必须按时间顺序进行编码,防止未来信息泄露。此时留一法或交叉验证也要基于时间顺序分割。

6. 完整代码示例

下面演示使用 category_encoders 在一个流水线中对比几种编码:

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
import category_encoders as ce

# 示例数据
data = pd.DataFrame({
    'city': ['BJ', 'SH', 'BJ', 'GZ', 'SH', 'SH', 'GZ', 'BJ', 'SZ', 'SZ'],
    'target': [1, 2, 1, 3, 2, 1, 3, 2, 4, 3]
})
X = data[['city']]
y = data['target']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 1. 独热编码
ohe = ce.OneHotEncoder(cols=['city'], use_cat_names=True)
X_train_ohe = ohe.fit_transform(X_train)
X_test_ohe = ohe.transform(X_test)

# 2. 目标编码(5折交叉验证,平滑10)
te = ce.TargetEncoder(cols=['city'], smoothing=10)
X_train_te = te.fit_transform(X_train, y_train)
X_test_te = te.transform(X_test)

# 3. 留一法目标编码
loo = ce.LeaveOneOutEncoder(cols=['city'])
X_train_loo = loo.fit_transform(X_train, y_train)
X_test_loo = loo.transform(X_test)

# 使用简单模型评估
model = LogisticRegression()
for name, X_tr, X_te in [('OneHot', X_train_ohe, X_test_ohe),
                         ('Target', X_train_te, X_test_te),
                         ('LOO', X_train_loo, X_test_loo)]:
    model.fit(X_tr, y_train)
    pred = model.predict(X_te)
    # 回归示例,若为分类可使用相应指标
    print(f"{name} 测试集 R^2: {model.score(X_te, y_test):.3f}")

结语

编码类别特征没有银弹,理解数据、模型特性以及泄露风险是选型的基础。从独热到目标编码,再到留一法,你掌握了一条逐步增强的路径:当基数上升时从独热转向目标编码,当要求严格防过拟合时优先采用留一法或交叉验证目标编码。配合专业的编码库,这些方法可以无缝集成到你的机器学习管道中,大幅提升模型对类别信息的利用效率。