ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

特征处理之OneHot(哑编码)

特征处理之OneHot(哑编码)

OneHot 编码(独热编码 / 哑编码),指的是把类别型特征转换成一组二进制(0/1)特征,每个类别对应一列。

它属于你之前看的组件列表里(特征工程组件 -> 特征处理组件 -> OneHot(哑编码))的核心功能。下面我从概念、原理、代码实现、典型场景几个方面来讲。


一、为什么需要 OneHot 编码?

机器学习模型(尤其是线性模型、神经网络)只能处理数值,不能直接处理字符串类别。

比如有一个"城市"特征:

城市
北京
上海
广州

如果简单地用 0/1/2 编码:

城市编码
0
1
2

问题:模型会误以为广州 > 上海 > 北京,存在"大小关系",但城市之间没有大小之分。

OneHot 编码解决的正是这个问题:把每个类别拆成一列独立的 0/1 特征,让模型知道类别之间是平等、无序的。


二、OneHot 编码的原理

以"城市"为例,有三种取值:北京、上海、广州。

OneHot 编码后:

北京上海广州
100
010
001
  • 每行只有一个位置是 1,其余是 0 → 这就是"独热"(One-Hot)的由来。

  • 有 N 个类别,就生成 N 列(或 N-1 列,见下文"哑变量陷阱")。


三、"OneHot"和"哑编码"的区别

严格来说:

名称含义列数
OneHot 编码每个类别一列,共 N 列N
哑编码(Dummy Encoding)去掉一列作为基准,共 N-1 列N-1

为什么去掉一列?

如果保留全部 N 列,会出现多重共线性(也叫"哑变量陷阱"):

  • 最后一列可以由前 N-1 列推导出来(因为每行只有一个 1)。

  • 这对线性回归、逻辑回归等模型是问题,会导致系数不唯一。

树模型(XGBoost、LightGBM)不受影响,可以保留 N 列。

注意:在实际工具里(sklearn、pandas),"OneHot"和"哑编码"经常混用,具体看参数。


四、pandas 实现

1.pd.get_dummies()—— 最常用

import pandas as pd df = pd.DataFrame({ 'city': ['北京', '上海', '广州', '北京', '上海'], 'age': [25, 30, 35, 40, 45] }) # OneHot 编码(保留全部列) df_onehot = pd.get_dummies(df, columns=['city']) print(df_onehot)

输出:

age city_北京 city_上海 city_广州 0 25 1 0 0 1 30 0 1 0 2 35 0 0 1 3 40 1 0 0 4 45 0 1 0

2. 去掉一列(哑编码)

# drop_first=True 去掉第一列,避免多重共线性 df_dummy = pd.get_dummies(df, columns=['city'], drop_first=True) print(df_dummy)

输出:

age city_上海 city_广州 0 25 0 0 1 30 1 0 2 35 0 1 3 40 0 0 4 45 1 0

3. 处理新数据(get_dummies的坑)

pd.get_dummies不适合生产环境,因为它每次都会根据当前数据的类别重新生成列。如果测试集缺少某个类别,列会对不上。

正确做法:用 sklearn 的OneHotEncoder,它能在训练集 fit 后,固定列结构。


五、sklearn 实现

1. 基本用法

from sklearn.preprocessing import OneHotEncoder import numpy as np X = np.array([['北京'], ['上海'], ['广州'], ['北京'], ['上海']]) # sparse_output=False 返回稠密数组(sklearn 1.2+) # 旧版本用 sparse=False encoder = OneHotEncoder(sparse_output=False) X_encoded = encoder.fit_transform(X) print(X_encoded) print("类别顺序:", encoder.categories_)

输出:

[[1. 0. 0.] [0. 1. 0.] [0. 0. 1.] [1. 0. 0.] [0. 1. 0.]] 类别顺序: [array(['上海', '北京', '广州'], dtype=object)]

注意:sklearn 会按字典序排列类别,不一定是原始顺序。

2. 去掉一列(哑编码)

encoder = OneHotEncoder(sparse_output=False, drop='first') X_encoded = encoder.fit_transform(X) print(X_encoded)

输出(去掉了"上海"这一列):

[[0. 0.] [1. 0.] [0. 1.] [0. 0.] [1. 0.]]

drop参数:

  • None:保留全部列(默认)

  • 'first':去掉每类第一列

  • 'if_binary':二分类时去掉一列

3. 处理新数据(transform)

X_new = np.array([['北京'], ['广州'], ['深圳']]) # 深圳是训练时没见过的类别 X_new_encoded = encoder.transform(X_new) print(X_new_encoded)

handle_unknown参数决定遇到未知类别怎么办:

# 默认 'error':遇到未知类别报错 # 'ignore':未知类别全部编码为 0 # 'infrequent_if_exist':归入低频类别 encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore') encoder.fit(X) X_new_encoded = encoder.transform(X_new) print(X_new_encoded)

输出(深圳全为 0):

[[0. 0.] [0. 1.] [0. 0.]]

4. 多列同时编码

python

import pandas as pd df = pd.DataFrame({ 'city': ['北京', '上海', '广州'], 'gender': ['男', '女', '男'] }) encoder = OneHotEncoder(sparse_output=False) X_encoded = encoder.fit_transform(df[['city', 'gender']]) print(X_encoded) print("所有类别:", encoder.categories_)

5. 指定类别顺序(避免训练/测试集列不一致)

encoder = OneHotEncoder( sparse_output=False, categories=[['北京', '上海', '广州']] # 手动指定类别顺序 )

这在生产环境很重要:训练集和测试集必须用同一套列顺序。


六、在 sklearn Pipeline 中使用

OneHot 通常只对类别列做,数值列用其他处理:

from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split import pandas as pd # 模拟数据 df = pd.DataFrame({ 'city': ['北京', '上海', '广州', '北京', '上海', '广州'], 'gender': ['男', '女', '男', '女', '男', '女'], 'age': [25, 30, 35, 40, 45, 50], 'income': [8000, 12000, 15000, 20000, 25000, 30000] }) y = [0, 1, 0, 1, 0, 1] X_train, X_test, y_train, y_test = train_test_split( df, y, test_size=0.3, random_state=42 ) # 类别列 → OneHot # 数值列 → 标准化 preprocessor = ColumnTransformer([ ('cat', OneHotEncoder(sparse_output=False, handle_unknown='ignore'), ['city', 'gender']), ('num', StandardScaler(), ['age', 'income']) ]) pipe = Pipeline([ ('prep', preprocessor), ('clf', LogisticRegression()) ]) pipe.fit(X_train, y_train) print("测试集准确率:", pipe.score(X_test, y_test))

七、OneHot 编码的优缺点

优点

  1. 消除类别间的虚假大小关系:北京不会被认为小于上海。

  2. 通用性强:几乎所有模型都能处理 0/1 特征。

  3. 可解释性好:每一列对应一个类别,系数容易理解。

缺点

  1. 维度爆炸:如果某列有 1000 个类别,就会生成 1000 列,稀疏且占内存。

  2. 稀疏性:大部分值是 0,浪费存储和计算。

  3. 类别多时效果差:高基数类别(如用户ID、商品ID)不适合 OneHot。

  4. 对树模型不友好:树模型在稀疏特征上分裂效率低,且 OneHot 后每次只能选一个类别,不如直接标签编码。


八、高基数类别的替代方案

如果类别太多(比如超过 50 个),OneHot 不合适,可以用:

方法说明
目标编码(Target Encoding)用类别的目标均值代替
频率编码(Frequency Encoding)用类别的出现频率代替
WOE 编码评分卡常用,用 log(好坏比) 代替
嵌入(Embedding)神经网络中把高基数类别映射为低维向量
哈希编码(Hashing)用哈希函数把类别映射到固定维度

九、关键提醒

  1. 训练集和测试集列必须一致:用OneHotEncoder在训练集 fit,测试集只 transform,不要重新 fit。

  2. 处理未知类别:生产环境一定设置handle_unknown='ignore',否则遇到新类别会报错。

  3. 高基数类别慎用 OneHot:类别超过 50 个时,考虑目标编码、频率编码或嵌入。

  4. 线性模型建议 drop 一列:避免多重共线性;树模型可以保留全部列。

  5. 不要对数值列做 OneHot:数值列应该分箱后再 OneHot,或直接标准化。

  6. OneHot 后维度会膨胀:注意内存和计算成本,必要时用稀疏矩阵(sparse_output=True)。


十、总结

OneHot 编码 = 把类别型特征变成一组 0/1 列。

  • pandas 实现:pd.get_dummies(),适合快速探索。

  • sklearn 实现:OneHotEncoder,适合生产 Pipeline。

  • 核心价值:消除类别间的虚假大小关系,让模型正确理解无序类别。

  • 主要代价:维度爆炸、稀疏性、高基数类别效果差。

  • 替代方案:目标编码、频率编码、WOE、嵌入、哈希编码。

  • 关键原则:训练集 fit,测试集 transform;处理未知类别;高基数类别换方法。

它和前面讲的"分箱"是互补的:分箱把连续值变离散,OneHot 把离散类别变 0/1,两者经常一起用在特征工程流水线中。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进