OneHot 编码(独热编码 / 哑编码),指的是把类别型特征转换成一组二进制(0/1)特征,每个类别对应一列。
它属于你之前看的组件列表里(特征工程组件 -> 特征处理组件 -> OneHot(哑编码))的核心功能。下面我从概念、原理、代码实现、典型场景几个方面来讲。
一、为什么需要 OneHot 编码?
机器学习模型(尤其是线性模型、神经网络)只能处理数值,不能直接处理字符串类别。
比如有一个"城市"特征:
| 城市 |
|---|
| 北京 |
| 上海 |
| 广州 |
如果简单地用 0/1/2 编码:
| 城市编码 |
|---|
| 0 |
| 1 |
| 2 |
问题:模型会误以为广州 > 上海 > 北京,存在"大小关系",但城市之间没有大小之分。
OneHot 编码解决的正是这个问题:把每个类别拆成一列独立的 0/1 特征,让模型知道类别之间是平等、无序的。
二、OneHot 编码的原理
以"城市"为例,有三种取值:北京、上海、广州。
OneHot 编码后:
| 北京 | 上海 | 广州 |
|---|---|---|
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 1 |
每行只有一个位置是 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 编码的优缺点
优点
消除类别间的虚假大小关系:北京不会被认为小于上海。
通用性强:几乎所有模型都能处理 0/1 特征。
可解释性好:每一列对应一个类别,系数容易理解。
缺点
维度爆炸:如果某列有 1000 个类别,就会生成 1000 列,稀疏且占内存。
稀疏性:大部分值是 0,浪费存储和计算。
类别多时效果差:高基数类别(如用户ID、商品ID)不适合 OneHot。
对树模型不友好:树模型在稀疏特征上分裂效率低,且 OneHot 后每次只能选一个类别,不如直接标签编码。
八、高基数类别的替代方案
如果类别太多(比如超过 50 个),OneHot 不合适,可以用:
| 方法 | 说明 |
|---|---|
| 目标编码(Target Encoding) | 用类别的目标均值代替 |
| 频率编码(Frequency Encoding) | 用类别的出现频率代替 |
| WOE 编码 | 评分卡常用,用 log(好坏比) 代替 |
| 嵌入(Embedding) | 神经网络中把高基数类别映射为低维向量 |
| 哈希编码(Hashing) | 用哈希函数把类别映射到固定维度 |
九、关键提醒
训练集和测试集列必须一致:用
OneHotEncoder在训练集 fit,测试集只 transform,不要重新 fit。处理未知类别:生产环境一定设置
handle_unknown='ignore',否则遇到新类别会报错。高基数类别慎用 OneHot:类别超过 50 个时,考虑目标编码、频率编码或嵌入。
线性模型建议 drop 一列:避免多重共线性;树模型可以保留全部列。
不要对数值列做 OneHot:数值列应该分箱后再 OneHot,或直接标准化。
OneHot 后维度会膨胀:注意内存和计算成本,必要时用稀疏矩阵(
sparse_output=True)。
十、总结
OneHot 编码 = 把类别型特征变成一组 0/1 列。
pandas 实现:
pd.get_dummies(),适合快速探索。sklearn 实现:
OneHotEncoder,适合生产 Pipeline。核心价值:消除类别间的虚假大小关系,让模型正确理解无序类别。
主要代价:维度爆炸、稀疏性、高基数类别效果差。
替代方案:目标编码、频率编码、WOE、嵌入、哈希编码。
关键原则:训练集 fit,测试集 transform;处理未知类别;高基数类别换方法。
它和前面讲的"分箱"是互补的:分箱把连续值变离散,OneHot 把离散类别变 0/1,两者经常一起用在特征工程流水线中。