4. 特征编码技术:标签编码、独热编码、目标编码、频率编码与WOE编码

咱们做机器学习,遇到的第一关往往就是:数据里全是文字,模型只认数字。这就得靠特征编码来搭桥。编码选得好,模型吃得饱;编码选得糙,调参调到老。今天我把五种最常用的编码方法掰开揉碎讲清楚。

4.1 标签编码

标签编码,说白了就是给每个类别分配一个整数。假设性别有男、女、未知,那就分别变成0、1、2。简单粗暴,一行代码搞定。

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
labels = ['男', '女', '未知']
le.fit(labels)
print(le.transform(labels))  # [0 1 2]

但我得提醒一句:标签编码隐含了顺序关系。比如“未知”被编码成2,模型可能会认为它比“男”(0)和“女”(1)都“大”。如果你的类别之间没有天然顺序,这就会引入错误偏见。

避坑指南:我曾经在一个犯罪预测项目中,把地区用标签编码了,结果模型学出来“地区编码越大,犯罪率越高”——纯属数字幻觉。后来换成独热编码,才纠正了这种虚假关联。

适合用标签编码的场景:只有两个类别(比如是否逾期)、或者类别确实有序(学历:小学<中学<大学)。

4.2 独热编码

独热编码是解决顺序问题的标准答案。每个类别变成一个独立的二进制列,只有当前样本属于该类别时对应位为1。比如性别男=[1,0],女=[0,1]。

import pandas as pd
df = pd.DataFrame({'城市': ['北京','上海','广州']})
pd.get_dummies(df, columns=['城市'])

结果长这样:

北京上海广州
100
010
001

它的代价是维度爆炸。如果城市有100个,那就新生成100列。我见过一个电商数据集,光商品类别就有5000个,用独热编码后内存直接爆掉。所以高基数类别千万不要无脑独热。

实用技巧:遇到高基数特征,可以先做频率分组——把出现次数少于阈值(比如100次)的类别归为"其他"。这样既保留了主要信息,又控制了维度。

4.3 目标编码

目标编码是个狠角色。它用目标变量(比如点击率、转化率)的统计量来替换原始类别。具体来说,计算每个类别下目标值的均值,然后用这个均值作为编码值。

# 假数据:用户ID(高基数) -> 是否点击
mean_enc = df.groupby('user_id')['click'].mean()
df['user_encoded'] = df['user_id'].map(mean_enc)

我个人的习惯是:只要遇到高基数类别特征(上千个值),优先试目标编码。它天然解决了维度灾难,而且能捕捉类别与目标之间的关联。

但有一个大坑:过拟合。如果某个类别只出现一两次,它的均值就会非常极端。比如某个用户只出现一次且点击了,那编码值就是1.0,模型就会死记这个样本。

我记得有一次做风控模型,用目标编码处理设备指纹特征(几万个不重复的设备ID),没做平滑处理,结果线上预测时新设备ID全部命中均值,效果直接崩了3个百分点。从此我牢记:目标编码必须配合交叉验证或贝叶斯平滑。

标准的做法是用K-fold目标编码:在每一折内只用训练集计算均值,避免数据泄露。也可以用平滑公式:

编码值 = (该类别目标值之和 + m * 全局均值) / (该类别样本数 + m)

其中m是平滑系数,m越大,编码值越接近全局均值。

4.4 频率编码

频率编码更简单:直接用每个类别出现的频次或频率替换原值。比如某个品牌出现1000次,编码成1000或者0.01(归一化后)。

freq = df['brand'].value_counts(normalize=True)
df['brand_freq'] = df['brand'].map(freq)

这种编码的潜台词是:频繁出现的类别通常更重要。我在处理用户行为数据时发现,用户点击的品牌频率往往与购买意愿正相关。

但你想想看,频率编码完全不依赖目标变量,所以不会有数据泄露问题,这是它最大的优势。缺点也很明显——它没办法区分两个出现次数相同但含义完全不同的类别。

编码方法是否使用目标变量处理高基数过拟合风险
标签编码否好低
独热编码否差低
目标编码是好高
频率编码否好低
WOE编码是好中

4.5 WOE编码

WOE(Weight of Evidence)编码来自信用评分模型,算是目标编码的升级版。它不仅考虑目标均值,还考虑好坏样本的比例关系。

# 对二分类目标,计算每个类别的WOE
import numpy as np
# 假设坏样本为1,好样本为0
df_group = df.groupby('category').agg(
    good=('target', lambda x: (x==0).sum()),
    bad=('target', lambda x: (x==1).sum())
)
df_group['woe'] = np.log(
    (df_group['good'] / df_group['good'].sum()) /
    (df_group['bad'] / df_group['bad'].sum())
)

说白了,WOE衡量的是:这个类别里好样本的相对比例比整体样本高多少(正WOE)还是低多少(负WOE)。它在逻辑回归里特别好用,因为WOE与log-odds是线性关系。

我建议在信贷、保险、风控这类要求可解释性的场景中,优先使用WOE编码。它的系数可以直接解释为“该类别使风险上升/下降了多少”。

注意:WOE要求每个类别下同时有好样本和坏样本。如果某个类别只有好样本,计算时会除以0。实际工程中我会给每个类别加一个很小的平滑值(比如0.5),避免无穷大。

4.6 如何选择:一张决策图

说了这么多,到底用哪个?我整理了一个流程图帮你拍板。

特征编码选择流程 类别特征 基数高(>50个类别)? 是 目标编码 或 频率编码 否 独热编码 是否可解释? 是 WOE编码 否 目标编码 + 平滑 💡 说明:高基数时首选目标编码或频率编码;需要可解释性则升级为WOE;低基数直接用独热编码。

总结一下我的经验:

  • 类别数 ≤ 5 且无顺序 → 独热编码,别整花哨的
  • 类别数 6~50 → 独热或目标编码都行,优先目标编码
  • 类别数 > 50 → 目标编码/频率编码/WOE,绝对不要用独热
  • 风控模型 → WOE编码是不二之选
  • 怕过拟合 → 频率编码最安全

嗯,编码方法选对了,模型训练就成功了一半。剩下的交给调参和特征交叉。你根据自己手头的特征卡,对着上面的图选就对了。