4、特征编码技术:标签编码、独热编码、目标编码、频率编码、WOE编码
好,咱们进入第四讲。特征编码,说白了就是把非数值型数据变成模型能吃的数字。你想想看,模型它只认识数字,什么「男」「女」、「北京」「上海」,它根本理解不了。所以这一步,是特征工程里绕不开的硬骨头。
我个人习惯把编码技术分成两类:无序类别和有序类别。处理方式完全不同,搞混了容易出大问题。下面我挨个讲,每种我都会给出代码示例和避坑指南。
4.1 标签编码(Label Encoding)
标签编码最简单,就是把每个类别映射成一个整数。比如「红、黄、蓝」变成 0、1、2。
我在项目中遇到过有人拿标签编码处理颜色,结果模型学出了「蓝色 > 黄色 > 红色」的排序关系。这显然不合理。所以标签编码只适合有序类别,比如「小、中、大」这种。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
data['education_encoded'] = le.fit_transform(data['education'])
# 小学→0,中学→1,大学→2
4.2 独热编码(One-Hot Encoding)
独热编码是处理无序类别的标准方法。每个类别变成一个独立的 0/1 列。比如颜色有红、黄、蓝,就变成三列:is_red、is_yellow、is_blue。
嗯,这里要注意:如果类别太多,维度会爆炸。比如「城市」有 100 个取值,就会多出 100 列。我建议类别超过 10 个时,先做频率编码或者分组,再用独热。
import pandas as pd
# 方法一:pandas 自带
df_encoded = pd.get_dummies(data, columns=['color'], drop_first=True)
# 方法二:sklearn
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, drop='first')
encoded = ohe.fit_transform(data[['color']])
drop='first' 去掉第一列,避免多重共线性。说白了就是少一列不影响信息,还能让模型更稳定。
4.3 目标编码(Target Encoding)
目标编码是我在 Kaggle 比赛里最常用的技巧之一。它的思路很简单:用目标变量的均值来替换类别。比如某个城市的用户转化率是 0.3,那这个城市就编码成 0.3。
但这里有个大坑——数据泄露。如果你用全量数据计算均值,模型会「偷看」到未来的信息。我建议一定要做交叉验证式的目标编码。
from category_encoders import TargetEncoder
# 推荐使用交叉验证版本
te = TargetEncoder(cols=['city'], smoothing=10)
# smoothing 参数控制正则化强度,防止过拟合
data['city_encoded'] = te.fit_transform(data['city'], data['target'])
4.4 频率编码(Frequency Encoding)
频率编码就是用类别出现的次数或频率来替换。比如「北京」出现了 1000 次,就编码成 1000。这个方法的好处是:不会过拟合,而且能捕捉到「稀有类别」的信息。
我个人的经验是:频率编码适合类别很多(比如 50 个以上)且目标变量与类别频次相关的场景。比如电商里「热门城市」和「冷门城市」的购买行为差异很大。
# 频率编码
freq_map = data['city'].value_counts(normalize=True) # 频率
data['city_freq'] = data['city'].map(freq_map)
# 或者用计数
count_map = data['city'].value_counts()
data['city_count'] = data['city'].map(count_map)
4.5 WOE编码(Weight of Evidence)
WOE 编码在金融风控领域用得特别多。它衡量的是「好客户」和「坏客户」在每个类别上的分布差异。公式有点复杂,但说白了就是:这个类别对目标变量的区分度有多大。
我记得在做一个信用评分卡项目时,WOE 编码帮我们提升了 5% 的 KS 值。它的好处是自带单调性,特别适合逻辑回归这类线性模型。
import numpy as np
def woe_encoding(data, feature, target):
# 计算每个类别的 WOE 值
grouped = data.groupby(feature)[target].agg(['count', 'sum'])
grouped['good'] = grouped['count'] - grouped['sum']
grouped['bad'] = grouped['sum']
# 计算分布
grouped['dist_good'] = grouped['good'] / grouped['good'].sum()
grouped['dist_bad'] = grouped['bad'] / grouped['bad'].sum()
# 计算 WOE
grouped['woe'] = np.log(grouped['dist_good'] / grouped['dist_bad'])
# 处理无穷值
grouped['woe'] = grouped['woe'].replace([np.inf, -np.inf], 0)
return grouped['woe'].to_dict()
woe_map = woe_encoding(data, 'city', 'is_default')
data['city_woe'] = data['city'].map(woe_map)
4.6 如何选择编码方式?
我整理了一个简单的决策表,方便你快速选择:
| 变量类型 | 类别数量 | 推荐编码 | 适用模型 |
|---|---|---|---|
| 有序类别 | 任意 | 标签编码 | 树模型、线性模型 |
| 无序类别 | < 10 | 独热编码 | 所有模型 |
| 无序类别 | 10-50 | 频率编码 + 独热 | 树模型、线性模型 |
| 无序类别 | > 50 | 目标编码 / 频率编码 | 树模型 |
| 风控场景 | 任意 | WOE 编码 | 逻辑回归 |
最后说一句:没有银弹。我每次做项目都会试 2-3 种编码方式,看哪个在验证集上效果最好。你想想看,数据分布千变万化,死守一种方法肯定不行。
下一讲我们会聊特征缩放——标准化和归一化。到时候我会分享一个我踩过的坑:为什么标准化之后模型反而变差了。咱们下期见。