4、特征工程(上):特征编码、特征缩放与特征交叉

大家好,我是你们的老朋友。今天咱们聊聊特征工程的上半部分。说实话,面试里这块问得特别多,因为它是数据挖掘里最见功力的环节。你模型调得再好,特征没处理好,照样白搭。我见过太多人把精力全花在调参上,结果特征一塌糊涂——嗯,这其实是个坑。

4.1 特征编码:让机器读懂分类变量

机器只认数字,不认文字。所以分类变量必须编码。常用的有两种:LabelEncoder 和 OneHotEncoder。

4.1.1 LabelEncoder:标签编码

说白了,就是把类别映射成整数。比如「红、黄、蓝」变成 0、1、2。简单粗暴,但有个问题——它引入了大小关系。0 < 1 < 2,机器会以为「蓝」比「红」大。这在树模型里还好,但在线性模型里就麻烦了。

⚠️ 避坑指南
我曾经在一个风控项目里,直接用 LabelEncoder 编码「学历」字段。结果模型学出来,学历越高风险越大——因为编码值越大。实际上硕士和博士的风险差不多,但模型硬生生学出了线性关系。后来换成 OneHotEncoder 才纠正过来。
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
labels = ['红', '黄', '蓝', '绿']
encoded = le.fit_transform(labels)
print(encoded)  # 输出: [2 1 0 3]

4.1.2 OneHotEncoder:独热编码

OneHotEncoder 就聪明多了。它把每个类别变成一个独立的二进制列。比如「红」变成 [1,0,0,0],「黄」变成 [0,1,0,0]。这样就没有大小关系了。

但代价是维度爆炸。如果某个特征有 100 个类别,就会多出 100 列。我建议你这么做:

  • 类别少于 10 个:用 OneHotEncoder
  • 类别多于 10 个:考虑 LabelEncoder + 树模型,或者做频次编码
  • 类别特别多(比如城市名):用目标编码(Target Encoding)
from sklearn.preprocessing import OneHotEncoder
import numpy as np

data = np.array([['红'], ['黄'], ['蓝'], ['绿']])
encoder = OneHotEncoder(sparse_output=False)
encoded = encoder.fit_transform(data)
print(encoded)
# 输出:
# [[1. 0. 0. 0.]
#  [0. 1. 0. 0.]
#  [0. 0. 1. 0.]
#  [0. 0. 0. 1.]]
💡 我的小技巧
我个人习惯先用 LabelEncoder 快速跑个基线,看看效果。如果模型表现不错,就不折腾了。如果效果差,再换成 OneHotEncoder。别一上来就搞复杂,先简单再迭代。

4.2 特征缩放:让数值站在同一起跑线

特征缩放解决的是量纲问题。比如年龄是 0-100,收入是 0-100 万,这两个特征直接丢进模型,收入会主导梯度更新。你想想看,这合理吗?

4.2.1 StandardScaler:标准化

StandardScaler 把数据变成均值为 0、标准差为 1 的分布。公式很简单:(x - mean) / std。它假设数据服从正态分布。如果数据有异常值,效果会受影响。

我在项目中遇到过一个问题:某个特征有极端值,标准化后大部分数据被压缩到 -0.5 到 0.5 之间,模型几乎学不到信息。后来我改用 RobustScaler 才解决。

from sklearn.preprocessing import StandardScaler
import numpy as np

data = np.array([[1], [2], [3], [4], [100]])  # 注意这个 100 是异常值
scaler = StandardScaler()
scaled = scaler.fit_transform(data)
print(scaled)
# 输出: [[-0.47], [-0.44], [-0.41], [-0.38], [ 1.71]]

4.2.2 MinMaxScaler:归一化

MinMaxScaler 把数据压缩到 [0, 1] 区间。公式:(x - min) / (max - min)。它对异常值非常敏感——如果有一个极大值,其他数据都会被压到很小。

什么时候用 MinMaxScaler?我建议:

  • 数据分布比较均匀,没有极端值
  • 需要把数据限制在固定区间(比如图像像素值 0-255 归一化到 0-1)
  • 模型对输入范围敏感(比如神经网络)
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
data = np.array([[1], [2], [3], [4], [100]])
scaled = scaler.fit_transform(data)
print(scaled)
# 输出: [[0.  ], [0.01], [0.02], [0.03], [1.  ]]
# 你看,前四个值几乎被压到 0 了
📌 核心对比
方法 适用场景 对异常值敏感度 输出范围
StandardScaler 数据近似正态分布 中等 无固定范围
MinMaxScaler 数据均匀分布 [0, 1]
RobustScaler 数据有异常值 无固定范围

4.3 特征交叉:创造高阶特征

特征交叉就是把两个或多个特征组合起来,创造新的特征。比如「性别」和「年龄段」交叉,可以产生「年轻女性」「中年男性」这样的组合特征。说白了,就是让模型能学到特征之间的交互关系。

为什么需要特征交叉?因为很多关系不是线性的。比如在电商场景里,「男性」和「化妆品」单独看相关性不高,但「年轻男性」和「化妆品」可能相关性很高——因为年轻男性也开始护肤了。这种交互关系,线性模型学不到,需要手动构造。

4.3.1 怎么做特征交叉

常用的方法有:

  • 笛卡尔积:把两个分类特征的所有组合都列出来。比如「颜色」×「尺寸」= 红小、红中、红大、蓝小……
  • 多项式特征:对数值特征做乘法。比如 age * income 可以反映「消费能力」。
  • 分箱后交叉:先把连续值分箱,再交叉。比如年龄分箱成「青年、中年、老年」,收入分箱成「低、中、高」,然后交叉。
from sklearn.preprocessing import PolynomialFeatures
import numpy as np

# 假设有两个特征:年龄和收入
data = np.array([[25, 5000],
                 [35, 8000],
                 [45, 12000]])

poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
crossed = poly.fit_transform(data)
print(crossed)
# 输出: [[25, 5000, 125000],
#        [35, 8000, 280000],
#        [45, 12000, 540000]]
# 第三列就是 age * income 的交叉特征
⚠️ 注意维度爆炸
特征交叉虽然强大,但会急剧增加特征数量。如果有 10 个特征,两两交叉就产生 45 个新特征。三三交叉?那是 120 个。我建议你:先用业务知识筛选出重要的交叉,别一股脑全做。我曾经在一个推荐系统项目里,做了所有特征的二阶交叉,结果特征从 20 个膨胀到 400 个,模型直接过拟合了。

4.3.2 什么时候用特征交叉

我个人经验是:

  • 线性模型:必须做交叉,否则学不到交互关系
  • 树模型:可以不做,因为树模型本身能自动学习交互(但做交叉有时也能提升)
  • 深度学习:看情况,有些网络结构(如 FM、DeepFM)能自动学习交叉
💡 面试常问
面试官可能会问:「为什么树模型不需要特征交叉?」
答案是:树模型通过分裂节点,天然能捕捉特征交互。比如第一层按性别分裂,第二层按年龄分裂,就相当于做了「性别×年龄」的交叉。但线性模型不行,它只能学加权和,所以需要手动构造交叉特征。

小结

特征工程的上半部分就这些。记住三个核心:

  1. 编码:分类变量要转数字,注意选择合适的方法
  2. 缩放:让特征量纲统一,注意异常值处理
  3. 交叉:创造高阶特征,但别过度

下一章咱们聊特征选择,看看怎么从一堆特征里挑出最有用的。到时候我会分享一个我踩过的坑——嗯,那个坑挺深的,差点让我加班到凌晨三点。


公众号:蓝海资料掘金营,微信deep3321