独热编码(One-hot Encoding)、标签编码(Label Encoding)、目标编码(Target Encoding)、序数编码(Ordinal Enconding)是机器学习领域非常常见的四个编码方式,也是初学者感到困惑和混淆的内容。本文从原理出发、渗透Python应用,详细解释这四种编码方式。
独热编码(One-hot Encoding)
独热编码主要用于无序的分类特征,生成多列的0/1稀疏矩阵。我们可以从范例了认识它。
假如我们有以下比赛的数据,其中比赛分数 和比赛等级 是数据的特征,是否获得奖金是预测值(分类预测):
| index | 比赛分数 | 比赛等级 | 是否获得奖金 |
|---|---|---|---|
| 0 | 178 | 三等 | 否 |
| 1 | 192 | 一等 | 是 |
| 2 | 184 | 二等 | 是 |
| 3 | 191 | 一等 | 是 |
| 4 | 172 | 三等 | 否 |
对于这组数据,比赛等级 和是否获得奖金 这两列显然是无序的分类特征(一等、二等、三等无数学大小含义),我们可以使用独热编码来使得机器学习模型能够理解它们的特征。当我们对这组数据的比赛等级 和是否获得奖金这两列进行独热编码后,数据表将会变为如下的样式。
| index | 比赛分数 | 比赛等级_一等 | 比赛等级_二等 | 比赛等级_三等 | 是否获得奖金_是 | 是否获得奖金_否 |
|---|---|---|---|---|---|---|
| 0 | 178 | 0 | 0 | 1 | 0 | 1 |
| 1 | 192 | 1 | 0 | 0 | 1 | 0 |
| 2 | 184 | 0 | 1 | 0 | 1 | 0 |
| 3 | 191 | 1 | 0 | 0 | 1 | 0 |
| 4 | 172 | 0 | 0 | 1 | 0 | 1 |
基于这个例子,我们可以看到独热编码会将分类特征拆分为各单独的一列以0/1的形式作是否存在这一特征的编码。由此我们应该知道用于独热编码的分类特征应该是比较少的(如本例中只有三个类别一等、二等、三等,或者是/否两个类别)
所以可以得出独热编码的优缺点:
| 优点 | 缺点 |
|---|---|
| 完全消除类别虚假数值顺序,适配线性模型; | 维度爆炸:若特征包含上百个类别,会新增上百列,数据极度稀疏,占用大量内存,训练速度大幅下降; |
| 特征含义清晰,可解释性强。 | 不适用高基数特征(用户 ID、城市、商品编号等)。 |
使用独热编码
常用进行独热编码可以使用pandas或者sklearn的相关方法:
Python
#pandas
import pandas as pd
df_encoded = pd.get_dummies(df,columns=['比赛等级','是否获得奖金'],drop_first=False)
Python
#scikit-learn
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('cat',OneHotEncoder(drop=None),['比赛等级','是否获得奖金'])
],
remainder='passthrough'
)
X_encoded = preprocessor.fit_transform(X)
'''
transformers 参数
name:字符串,给这个转换器起个名字,便于调试或查看特征名(如 'cat')。
transformer:要应用的转换器对象,这里是 OneHotEncoder(drop=None)。
columns:要应用该转换器的列名列表,如 ['比赛等级','是否获得奖金']
remainder 参数
控制未被 transformers 选中的列如何处理:
'drop'(默认):丢弃未选中的列。
'passthrough':保留未选中的列原样输出。
也可以是一个转换器对象,对剩余列应用其他转换。
'''
标签编码(Label Encoding)
在独热编码的使用过程中会发现,当分类特征的种类过多时,数据会变得极度稀疏,这时的训练效率会变得非常低。于是,使用标签编码就可以解决这一痛点。
标签编码自动的将所有类别随机映射成连续整数0,1,2.....
我们来看以下例子:
| index | 比赛分数 | 比赛等级 | 是否获得奖金 |
|---|---|---|---|
| 0 | 178 | 三等 | 否 |
| 1 | 192 | 一等 | 是 |
| 2 | 184 | 二等 | 是 |
| 3 | 191 | 一等 | 是 |
| 4 | 172 | 三等 | 否 |
对于这组数据,对比赛等级和是否获得奖金两列进行标签编码会转换为如下样式:
| index | 比赛分数 | 比赛等级 | 是否获得奖金 |
|---|---|---|---|
| 0 | 178 | 0 | 0 |
| 1 | 192 | 2 | 1 |
| 2 | 184 | 1 | 1 |
| 3 | 191 | 2 | 1 |
| 4 | 172 | 0 | 0 |
但是,标签编码有一个明显的缺点 。对于没有数学意义上的大小关系的数据会引入虚假有序关系。如上面这个例子,它可能让模型误以为一等(2)>二等(1)>三等(0)。这样会让神经网络或者线性模型遭到误导,影响训练效果。
如果使用树模型,这种影响则偏小。
使用标签编码
Python
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
data["奖金标签"] = le.fit_transform(data["是否获得奖金"])
print(data["奖金标签"]) # [0,1,1,1,0]
目标编码(Target Encoding)
目标编码属于有监督编码,利用训练集目标 y 的统计均值替换分类类别,解决高基数特征维度爆炸问题。一般来说,为了保持特征的训练平衡,目标编码会使用有权重的均值(加权平均值)作为分类特征的编码。
\(加权平均值 = \frac{n \times 分项平均值 + m \times 总体平均值 }{n+m}\)
其中 \(n:分项平均值的权重(一般为数据行数)\)
\(m:总体平均值的权重(由用户自行设定)\)
| index | 比赛分数 | 比赛等级 | 是否获得奖金 | 奖金标签 y |
|---|---|---|---|---|
| 0 | 178 | 三等 | 否 | 0 |
| 1 | 192 | 一等 | 是 | 1 |
| 2 | 184 | 二等 | 是 | 1 |
| 3 | 191 | 一等 | 是 | 1 |
| 4 | 172 | 三等 | 否 | 0 |
按照上面的例子,我们对比赛等级 这一列进行目标编码,将得到如下的效果:
首先先计算总体平均值:
\总体平均值 = \\frac{0+1+1+1+0}{5} = 0.6 \\
| 比赛等级 | n | 分项平均值 | 总体平均值 | m | 加权平均值 |
|---|---|---|---|---|---|
| 一等 | 2 | 1.0 | 0.6 | 1 | \(\frac{2 \times 1.0 + 1 \times 0.6}{2+1}=0.867\) |
| 二等 | 1 | 1.0 | 0.6 | 1 | \(\frac{1 \times 1.0 + 1 \times 0.6}{1+1}=0.800\) |
| 三等 | 2 | 0.0 | 0.6 | 1 | \(\frac{2 \times 0.0 + 1 \times 0.6}{2+1}=0.200\) |
用这些加权平均值替换原来的"比赛等级",得到目标编码后的数据:
| index | 比赛分数 | 比赛等级_目标编码 | 是否获得奖金 |
|---|---|---|---|
| 0 | 178 | 0.200 | 否 |
| 1 | 192 | 0.867 | 是 |
| 2 | 184 | 0.800 | 是 |
| 3 | 191 | 0.867 | 是 |
| 4 | 172 | 0.200 | 否 |
使用目标编码
使用 category_encoders
python
# pip install category_encoders
from category_encoders import TargetEncoder
encoder = TargetEncoder(cols=["比赛等级"], smoothing=1.0)
data_encoded = encoder.fit_transform(data[["比赛等级"]], data["奖金标签"])
data["比赛等级_目标编码"] = data_encoded["比赛等级"]
print(data)
使用 scikit-learn
python
# scikit-learn 1.3+ 支持 TargetEncoder
from sklearn.preprocessing import TargetEncoder
encoder = TargetEncoder(target_type="binary", smooth=1.0)
X_encoded = encoder.fit_transform(data[["比赛等级"]], data["奖金标签"])
print(X_encoded)
目标编码优缺点
| 优点 | 缺点 |
|---|---|
| 能处理高基数分类特征,不会像独热编码一样维度爆炸 | 容易造成目标泄漏,训练集内直接编码会过拟合 |
| 利用目标 y 的统计信息,特征表达能力强 | 小样本类别的统计量不稳定,需要平滑处理 |
| 适合树模型、线性模型、神经网络等多种模型 | 需要交叉验证、留一法或嵌套编码,流程更复杂 |
| 不增加特征维度,内存占用和训练速度通常优于独热编码 | 对训练集和测试集分布变化敏感,新类别需要回退到全局均值 |
序数编码(Ordinal Encoding)
序数编码用于有序分类特征。它会按照业务上明确的顺序,把类别映射成整数,从而保留类别之间的大小或顺序关系。
序数编码和标签编码看起来很像,都是把类别变成整数。区别在于:
- 标签编码:通常只是自动或随机地把类别映射成 0、1、2......不保证整数顺序有业务含义;
- 序数编码:由用户显式指定类别顺序,整数顺序具有明确含义。
例如,如果业务认为"比赛等级"存在如下顺序:
\三等 \< 二等 \< 一等 \\
那么可以使用序数编码:
| index | 比赛分数 | 比赛等级 | 是否获得奖金 | 比赛等级_序数 |
|---|---|---|---|---|
| 0 | 178 | 三等 | 否 | 0 |
| 1 | 192 | 一等 | 是 | 2 |
| 2 | 184 | 二等 | 是 | 1 |
| 3 | 191 | 一等 | 是 | 2 |
| 4 | 172 | 三等 | 否 | 0 |
这里:
- 三等 → 0
- 二等 → 1
- 一等 → 2
这样模型就能知道"一等"比"二等"高,"二等"比"三等"高。但如果特征本身没有顺序,比如"颜色""城市""商品编号",就不应该使用序数编码,否则会引入虚假的顺序关系。
使用序数编码
使用 scikit-learn
python
from sklearn.preprocessing import OrdinalEncoder
encoder = OrdinalEncoder(categories=[["三等", "二等", "一等"]])
data["比赛等级_序数"] = encoder.fit_transform(data[["比赛等级"]])
print(data["比赛等级_序数"].tolist()) # [0.0, 2.0, 1.0, 2.0, 0.0]
注意:categories 中的顺序就是编码顺序,第一个类别编码为 0,第二个类别编码为 1,以此类推。
使用 pandas 手动映射
python
order_map = {
"三等": 0,
"二等": 1,
"一等": 2
}
data["比赛等级_序数"] = data["比赛等级"].map(order_map)
print(data)
序数编码优缺点
| 优点 | 缺点 |
|---|---|
| 保留有序类别之间的顺序关系 | 只适用于确实存在顺序的分类特征 |
| 不增加特征维度,内存占用小 | 顺序必须由业务确定,错误顺序会误导模型 |
| 编码结果可解释,适合树模型和部分线性模型 | 默认假设类别之间等距,但实际间隔不一定相等 |
| 可以配合线性模型表达单调关系 | 对无序类别使用会引入虚假顺序 |
四种编码方式简单对比
| 编码方式 | 适用场景 | 是否保留顺序 | 是否增加维度 | 是否使用目标 y |
|---|---|---|---|---|
| 独热编码 One-hot Encoding | 无序、低基数分类特征 | 否 | 是 | 否 |
| 标签编码 Label Encoding | 无序特征、树模型,或目标变量编码 | 否 | 否 | 否 |
| 目标编码 Target Encoding | 高基数分类特征、有监督任务 | 不直接保留 | 否 | 是 |
| 序数编码 Ordinal Encoding | 有序分类特征 | 是 | 否 | 否 |