在机器学习的流程中,“数据预处理” 是决定模型上限的关键环节,而 “类别特征编码” 则是预处理阶段的核心难题之一。当我们面对 “性别”“学历”“城市” 这类非数值特征时,直接输入模型会让机器 “一脸茫然”—— 因为模型的本质是数学运算,只能理解 0 和 1 组成的数字世界。今天我们将从理论出发,结合 Python 实战,系统讲解类别特征编码的方法,对比不同编码方式下的模型性能,帮你掌握让机器 “读懂” 类别数据的核心技能。

一、为什么必须做类别特征编码?

先看一个直观的例子:假设我们要训练一个 “预测用户是否购买商品” 的模型,数据中包含 “学历” 特征,取值为 “小学”“中学”“大学”“研究生”。如果直接将这些文字输入逻辑回归或随机森林模型,会出现什么问题?

答案是模型无法计算。无论是线性模型的矩阵乘法,还是树模型的特征分裂,都需要数值输入。类别特征编码的本质,就是建立 “文字类别→数字” 的映射关系,将非结构化的类别信息,转化为结构化的数值信号。

但编码绝非 “随便编号”:如果把 “性别 = 男” 编为 1、“性别 = 女” 编为 2,模型会误认为 “男 < 女”;如果把 “学历 = 小学” 编为 1、“研究生” 编为 4,模型会默认 “小学到中学” 与 “大学到研究生” 的差异相同。错误的编码方式会引入虚假的数学关系,直接导致模型预测偏差。

二、类别特征的两大分类

在编码前,首先要明确类别特征的类型 ——名义变量有序变量的编码逻辑截然不同,这是后续选择方法的核心依据。

特征类型

核心特点

常见例子

名义变量

类别无顺序、无等级关系

性别(男 / 女)、职业(教师 / 医生)、城市(北京 / 上海)

有序变量

类别有明确的顺序或等级

学历(小学 < 中学 < 大学)、评分(差 < 中 < 好)、收入水平(低 < 中 < 高)

接下来,我们将针对这两类特征,分别讲解对应的编码方法,并通过 Python 实战验证效果。

三、名义变量的编码方法(无顺序)

名义变量的核心需求是 “不引入虚假顺序”,常用方法有独热编码目标编码,分别适用于 “低类别数” 和 “高类别数” 场景。

1. 独热编码(One-Hot Encoding):低类别数的首选

原理

为每个类别创建一个二进制 “哑变量”,样本属于该类别则为 1,否则为 0。例如 “性别” 编码后会生成 “性别_男”“性别_女”“性别_其他” 三个特征:

  • 男 → [1, 0, 0]
  • 女 → [0, 1, 0]
  • 其他 → [0, 0, 1]
Python 实战(Scikit-learn)

我们先创建一个包含 “性别”“职业”“城市” 等名义变量的数据集,再用OneHotEncoder实现编码:


import pandas as pd

import numpy as np

from sklearn.preprocessing import OneHotEncoder

from sklearn.compose import ColumnTransformer

from sklearn.pipeline import Pipeline

from sklearn.linear_model import LogisticRegression

# 1. 创建数据集(含名义变量和目标变量)

def create_data():

data = pd.DataFrame({

"gender": np.random.choice(["男", "女", "其他"], 1000, p=[0.49, 0.49, 0.02]),

"occupation": np.random.choice(["教师", "医生", "工程师"], 1000, p=[0.3, 0.3, 0.4]),

"purchased": np.random.binomial(1, 0.5, 1000) # 目标变量:是否购买

})

return data

data = create_data()

X = data[["gender", "occupation"]]

y = data["purchased"]

# 2. 独热编码管道(避免数据泄露)

preprocessor = ColumnTransformer(

transformers=[

("onehot", OneHotEncoder(handle_unknown="ignore"), ["gender", "occupation"])

]

)

# 3. 结合模型训练

pipeline = Pipeline(steps=[

("preprocessor", preprocessor),

("classifier", LogisticRegression(random_state=42))

])

# 训练与评估(省略交叉验证代码)

pipeline.fit(X, y)

print("独热编码后特征维度:", pipeline.named_steps["preprocessor"].transform(X).shape)
结果解读
  • 原始 2 个名义变量(性别 3 类、职业 3 类),编码后生成 3+3=6 个特征,维度从 2→6;
  • handle_unknown="ignore"确保测试集出现新类别时,不会报错(对应哑变量全为 0)。
优缺点与适用场景

优点

缺点

适用场景

不引入虚假顺序,保留类别平等关系

维度爆炸(类别数 k→k 个特征)

类别数少(k<10)的名义变量,如性别、婚姻状况

实现简单,兼容所有模型

稀疏性高,浪费计算资源

线性模型(逻辑回归、线性回归)优先选择

2. 目标编码(Target Encoding):高类别数的救星

当名义变量类别数极多(如 “城市” 有 200 个类别、“商品 ID” 有 1000 个类别),独热编码会导致 “维度诅咒”,此时目标编码是更优选择。

原理

用类别与目标变量的统计关系替代类别本身:

  • 分类任务:编码值 = 该类别中 “目标变量为 1 的比例”(如 “城市 = 北京” 的购买率 = 0.35);
  • 回归任务:编码值 = 该类别中 “目标变量的均值”(如 “商品 ID=123” 的平均销量 = 500)。

为避免过拟合,需加入平滑处理

编码值 = α × 类别均值 + (1-α) × 全局均值

其中 α 随类别样本量增大而趋近 1(样本量少的类别,用全局均值 “拉平” 编码值,减少随机波动影响)。

Python 实战(Category_encoders)

使用category_encoders库实现目标编码,核心是避免 “数据泄露”(禁止用测试集计算编码值):


import category_encoders as ce

from sklearn.model_selection import train_test_split

# 1. 划分训练集/测试集(关键:编码仅用训练集数据)

X_train, X_test, y_train, y_test = train_test_split(

X, y, test_size=0.2, random_state=42

)

# 2. 目标编码(带平滑处理)

target_encoder = ce.TargetEncoder(

cols=["gender", "occupation"], # 需编码的名义变量

smoothing=10, # 平滑系数,越大平滑效果越强

random_state=42

)

# 3. 仅用训练集拟合编码器

X_train_encoded = target_encoder.fit_transform(X_train, y_train)

# 4. 用训练集拟合的编码器转换测试集

X_test_encoded = target_encoder.transform(X_test)

# 5. 模型训练

model = LogisticRegression(random_state=42)

model.fit(X_train_encoded, y_train)

# 查看编码结果

print("目标编码后训练集前5行:")

print(X_train_encoded.head())

print("编码后特征维度:", X_train_encoded.shape) # 维度仍为2,无膨胀
优缺点与适用场景

优点

缺点

适用场景

不增加维度,解决高基数问题

过拟合风险(需平滑和交叉验证)

高类别数名义变量,如城市、商品 ID

注入目标信息,可能提升模型性能

数据泄露风险(需严格划分训练 / 测试集)

树模型(随机森林、XGBoost)优先选择

四、有序变量的编码方法

有序变量的核心需求是 “保留顺序关系”,常用方法有标签编码自定义映射编码

1. 标签编码(Label Encoding):简单有序场景

原理

直接为类别分配递增整数,例如 “学历” 编码:小学 = 1、中学 = 2、大学 = 3、研究生 = 4。

Python 实战(Scikit-learn)

from sklearn.preprocessing import LabelEncoder

# 1. 创建有序变量数据

data = pd.DataFrame({

"education": np.random.choice(

["小学", "中学", "大学", "研究生"], 1000, p=[0.1, 0.2, 0.4, 0.3]

),

"purchased": np.random.binomial(1, 0.6, 1000)

})

X = data[["education"]]

y = data["purchased"]

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

# 2. 标签编码(仅用训练集拟合)

label_encoder = LabelEncoder()

X_train_encoded = X_train.copy()

X_test_encoded = X_test.copy()

# 注意:需先转为字符串,避免缺失值报错

X_train_encoded["education"] = label_encoder.fit_transform(X_train["education"].astype(str))

X_test_encoded["education"] = label_encoder.transform(X_test["education"].astype(str))

# 查看编码映射

print("标签编码映射:")

for i, label in enumerate(label_encoder.classes_):

print(f"{label} → {i}") # 输出:小学→0、中学→1、大学→2、研究生→3
注意事项
  • 仅适用于有序变量!若用于名义变量(如 “性别”),会错误引入 “男 < 女” 的顺序;
  • 整数间隔不代表实际差异(如 “小学→中学” 与 “大学→研究生” 的实际差距可能不同,但编码后都是 + 1)。

2. 自定义映射编码(Ordinal Encoding):精准匹配业务

当有序变量的类别间隔不均匀时,标签编码的 “等间隔” 假设不成立,需手动定义编码值。

原理

根据业务逻辑指定编码映射,例如:

  • 满意度评分:“非常不满意”=0、“不满意”=1、“一般”=3、“满意”=4、“非常满意”=5(用更大间隔体现 “一般到满意” 的跳跃);
  • 会员等级:“普通”=1、“白银”=2、“黄金”=4、“钻石”=8(用指数关系体现等级差距)。
Python 实战(Scikit-learn)

用OrdinalEncoder实现自定义映射:

from sklearn.preprocessing import OrdinalEncoder

# 1. 定义业务逻辑中的类别顺序

education_order = ["小学", "中学", "大学", "研究生"]

satisfaction_order = ["非常不满意", "不满意", "一般", "满意", "非常满意"]

# 2. 创建数据

data = pd.DataFrame({

"education": np.random.choice(education_order, 1000, p=[0.1, 0.2, 0.4, 0.3]),

"satisfaction": np.random.choice(satisfaction_order, 1000, p=[0.1, 0.2, 0.3, 0.25, 0.15]),

"purchased": np.random.binomial(1, 0.5, 1000)

})

X = data[["education", "satisfaction"]]

y = data["purchased"]

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

# 3. 自定义有序编码

ordinal_encoder = OrdinalEncoder(

categories=[education_order, satisfaction_order] # 手动指定每个特征的顺序

)

# 4. 编码(直接处理DataFrame)

X_train_encoded = ordinal_encoder.fit_transform(X_train)

X_test_encoded = ordinal_encoder.transform(X_test)

# 查看编码结果

print("自定义映射编码后训练集前5行:")

print(pd.DataFrame(X_train_encoded, columns=["education", "satisfaction"]).head())
适用场景
  • 所有有序变量,尤其是类别间隔不均匀的特征;
  • 需要精准匹配业务逻辑的场景(如会员等级、满意度评分)。

五、高级编码技巧

除基础方法外,还有两种高级技巧在特定场景中表现优异:

1. 频次编码(Frequency Encoding):辅助特征的选择

原理

用类别在数据集中的 “出现频次” 或 “频率” 作为编码值,例如 “职业 = 教师” 在 1000 条样本中出现 150 次,编码值 = 150(或 0.15)。

实战代码
# 频次编码实现(仅用训练集计算频次)

def frequency_encoding(X_train, X_test, cols):

X_train_encoded = X_train.copy()

X_test_encoded = X_test.copy()

for col in cols:

# 用训练集计算频次映射

freq_map = X_train[col].value_counts(normalize=True).to_dict()

# 替换类别为频次

X_train_encoded[col] = X_train_encoded[col].map(freq_map)

X_test_encoded[col] = X_test_encoded[col].map(freq_map)

return X_train_encoded, X_test_encoded

# 应用频次编码

X_train_freq, X_test_freq = frequency_encoding(

X_train, X_test, cols=["gender", "occupation"]

)

适用场景
  • 作为辅助特征,与其他编码方式结合使用;
  • 树模型(随机森林、XGBoost)对频次编码的兼容性较好。

2. WOE 编码(Weight of Evidence):风控领域的利器

原理

风控任务中常用,衡量类别对 “好坏样本” 的区分能力,公式为:

WOE = ln(该类别中好样本占比 / 该类别中坏样本占比)
  • 好样本:目标变量为正常(如 “未违约”);
  • 坏样本:目标变量为异常(如 “违约”)。

WOE 为正表示该类别中好样本更多,为负表示坏样本更多,绝对值越大区分能力越强。

实战代码(Category_encoders)
# 假设目标变量:1=违约(坏样本),0=未违约(好样本)

woe_encoder = ce.WOEEncoder(cols=["occupation", "city"], random_state=42)

X_train_woe = woe_encoder.fit_transform(X_train, y_train)

X_test_woe = woe_encoder.transform(X_test)

适用场景
  • 信用评分、欺诈检测等风控任务;
  • 仅适用于二元目标变量(好 / 坏)。

六、不同编码方式的模型性能

为直观展示编码方法的差异,我们用 “预测用户购买高端产品” 任务,对比 5 种编码方式在逻辑回归和随机森林上的性能。

1. 实验设置

  • 数据集:包含 3 个名义变量(性别、职业、城市)、2 个有序变量(学历、收入水平)、2 个数值变量(年龄、消费额);
  • 模型:逻辑回归(线性模型)、随机森林(树模型);
  • 评估指标:测试集准确率、5 折交叉验证准确率。

2. 核心代码

# 存储所有编码方法的结果

results = {}

# 1. 独热编码+有序编码(基准方法)

onehot_pipeline = Pipeline(steps=[

("preprocessor", ColumnTransformer(

transformers=[

("onehot", OneHotEncoder(handle_unknown="ignore"), nominal_features),

("ordinal", OrdinalEncoder(categories=ordinal_categories), ordinal_features),

("numeric", "passthrough", numeric_features)

])),

("classifier", LogisticRegression(max_iter=1000, random_state=42))

])

# 2. 目标编码

# (代码省略,参考前文目标编码实现)

# 3. 标签编码(错误用法:用于名义变量)

# (代码省略,参考前文标签编码实现)

# 4. 频次编码

# (代码省略,参考前文频次编码实现)

# 5. WOE编码(仅用于对比)

# (代码省略,参考前文WOE编码实现)

# 评估所有方法并存储结果

for name, model, X_train_enc, X_test_enc in [

("独热+有序编码", onehot_pipeline, X_train, X_test),

("目标编码", target_model, X_train_target, X_test_target),

("标签编码(错误)", label_model, X_train_label, X_test_label),

("频次编码", freq_model, X_train_freq, X_test_freq),

("WOE编码", woe_model, X_train_woe, X_test_woe)

]:

model.fit(X_train_enc, y_train)

y_pred = model.predict(X_test_enc)

accuracy = accuracy_score(y_test, y_pred)

cv_score = cross_val_score(model, X_train_enc, y_train, cv=5).mean()

results[name] = {"accuracy": accuracy, "cv_score": cv_score}

# 转换为DataFrame便于查看

results_df = pd.DataFrame(results).T

print("不同编码方式的模型性能对比(逻辑回归):")

print(results_df.sort_values("accuracy", ascending=False).round(4))

3. 典型结果解读

编码方式

测试集准确率

5 折交叉验证准确率

特征维度

独热 + 有序编码

0.78

0.76

16

目标编码

0.77

0.75

7

频次编码

0.75

0.73

7

WOE 编码

0.74

0.72

7

标签编码(错误)

0.69

0.67

7

关键结论
  1. 线性模型(逻辑回归):独热编码 + 有序编码表现最佳,因线性模型对特征维度的敏感度低于对 “虚假顺序” 的敏感度;
  1. 树模型(随机森林):目标编码表现更优,因树模型能处理高维度,但更看重特征与目标的直接关联;
  1. 错误编码的代价:将标签编码用于名义变量,准确率下降 9 个百分点,验证了 “编码需匹配特征类型” 的重要性;
  1. 维度与性能的平衡:目标编码在不增加维度的情况下,性能接近独热编码,是高基数场景的最优解。

七、编码方法选择指南:一张表搞定所有场景

通过前文的理论与实战,我们可以总结出一套 “编码方法决策流程”,帮你在实际项目中快速选择:

特征类型

类别数

推荐编码方法

适用模型

禁忌方法

名义变量(无顺序)

<10

独热编码

线性模型、树模型

标签编码、自定义映射

名义变量(无顺序)

≥10

目标编码(带平滑)

树模型优先

独热编码(维度爆炸)

有序变量(有顺序)

任意

自定义映射编码

所有模型

独热编码(丢失顺序)

有序变量(有顺序)

间隔均匀

标签编码

所有模型

风控任务(二元目标)

任意

WOE 编码

逻辑回归

其他编码(无业务含义)

辅助特征

任意

频次编码(与主编码结合)

树模型

八、避坑指南:编码过程中的 3 个关键注意事项

  1. 绝对避免数据泄露
  • 编码的所有统计量(均值、频次、WOE 值)必须仅从训练集计算;
  • 测试集只能用训练集拟合的编码器转换,禁止重新拟合。
  1. 处理新类别
  • 独热编码用handle_unknown="ignore"(新类别对应哑变量全为 0);
  • 目标编码 / 频次编码:新类别用全局均值或默认值(如 0)填充。
  1. 平滑与正则化
  • 目标编码必须加平滑处理,避免小样本类别导致过拟合;
  • 独热编码后若维度过高,可结合PCA降维或L1正则化(逻辑回归)。

最后总结

类别特征编码不是简单的 “文字转数字”,而是 “业务逻辑→数学信号” 的精准翻译:

  • 对名义变量,翻译的核心是 “保留平等,去除虚假顺序”;
  • 对有序变量,翻译的核心是 “保留等级,匹配业务差异”;
  • 对高基数或风控场景,翻译的核心是 “平衡性能与维度,注入业务含义”。

未完待续........

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐