数值特征处理:标准化、归一化与非线性变换
数值特征,说白了就是那些能直接拿尺子量、拿秤称的数据——年龄、收入、温度、点击次数。这类特征在机器学习里最常见,但也是最容易出问题的。我见过太多新手直接把原始数值扔进模型,结果模型要么收敛慢得像蜗牛,要么干脆学偏了。
为什么会这样?因为不同特征的量纲和分布差异太大了。比如年龄范围是0-100,收入可能是0-100万,这两个特征放在一起,模型会天然认为收入更重要——仅仅因为它的数值更大。这显然不合理。
所以,数值特征处理的核心目标就两个:统一尺度和改善分布。下面我把常用的方法挨个讲清楚。
标准化(Z-score)
标准化是最常用的方法。它的公式很简单:
x' = (x - μ) / σ
其中μ是均值,σ是标准差。处理后,数据变成均值为0、标准差为1的分布。
我个人习惯在以下场景优先用标准化:
- 数据本身近似正态分布——标准化后效果最好
- 模型需要计算距离或梯度——比如SVM、逻辑回归、神经网络
- 特征中有明显的异常值——标准化对异常值没那么敏感
归一化(Min-Max)
归一化把数据压缩到[0, 1]区间:
x' = (x - min) / (max - min)
这个方法我一般在以下情况用:
- 数据有明确的上下界——比如像素值0-255
- 模型对数值范围敏感——比如KNN、神经网络
- 需要保留原始数据的相对关系
RobustScaler
RobustScaler是我处理异常值时的首选。它使用中位数和四分位距:
x' = (x - median) / IQR
其中IQR是上四分位数与下四分位数的差值。
它的优势很明显:
- 不受异常值影响——中位数和IQR对极端值不敏感
- 保留数据的相对排序
- 适合有大量噪声的数据
我记得有一次做金融风控模型,用户的收入特征里有几个亿万富翁,用Min-Max归一化后,99%的用户收入都被压到了0.01以下。换成RobustScaler后,问题立刻解决了。
非线性变换
有时候,标准化和归一化都不够用。比如数据严重偏态——长尾分布、指数分布,这时候就需要非线性变换。
对数变换(Log)
对数变换是最简单的非线性变换:
x' = log(x + 1)
加1是为了处理x=0的情况。它能把右偏的数据拉近正态分布。
我常用的场景:
- 收入、房价、点击量——这些数据通常呈长尾分布
- 比例数据——比如转化率、点击率
- 数据跨度很大——比如从1到100万
Box-Cox变换
Box-Cox是对数变换的推广,它有一个参数λ可以自动优化:
x' = (x^λ - 1) / λ, λ ≠ 0
x' = log(x), λ = 0
它的好处是:
- 自动寻找最佳变换参数
- 能处理多种偏态
- 有统计理论基础
Yeo-Johnson变换
Yeo-Johnson是Box-Cox的改进版,它不需要数据为正:
对于 x ≥ 0: x' = ((x+1)^λ - 1) / λ, λ ≠ 0
x' = log(x+1), λ = 0
对于 x < 0: x' = -(((-x+1)^(2-λ) - 1) / (2-λ)), λ ≠ 2
x' = -log(-x+1), λ = 2
说实话,公式看着复杂,但实际用起来很简单——scikit-learn里一行代码搞定。我一般在数据包含负数时用Yeo-Johnson,比如差值、变化量这类特征。
方法对比
| 方法 | 适用场景 | 对异常值敏感度 | 输出范围 | 数据要求 |
|---|---|---|---|---|
| 标准化(Z-score) | 近似正态分布、梯度模型 | 中等 | 无固定范围 | 无 |
| 归一化(Min-Max) | 有界数据、KNN | 高 | [0, 1] | 无 |
| RobustScaler | 含异常值的数据 | 低 | 无固定范围 | 无 |
| 对数变换 | 右偏分布、长尾数据 | 低 | 无固定范围 | 数据 > 0 |
| Box-Cox | 自动优化变换参数 | 低 | 无固定范围 | 数据 > 0 |
| Yeo-Johnson | 含负数的数据 | 低 | 无固定范围 | 无 |
知识体系图
下面这张图总结了数值特征处理的整体脉络:
代码示例
下面用scikit-learn演示几种方法的用法:
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler, PowerTransformer
# 模拟数据:包含异常值
data = np.array([[100], [200], [300], [400], [5000]]).astype(float)
# 标准化
scaler_std = StandardScaler()
data_std = scaler_std.fit_transform(data)
print("标准化:", data_std.flatten())
# 归一化
scaler_mm = MinMaxScaler()
data_mm = scaler_mm.fit_transform(data)
print("归一化:", data_mm.flatten())
# RobustScaler
scaler_rob = RobustScaler()
data_rob = scaler_rob.fit_transform(data)
print("RobustScaler:", data_rob.flatten())
# Box-Cox变换(要求数据为正)
pt_boxcox = PowerTransformer(method='box-cox')
data_bc = pt_boxcox.fit_transform(data)
print("Box-Cox:", data_bc.flatten())
# Yeo-Johnson变换(支持负数)
pt_yj = PowerTransformer(method='yeo-johnson')
data_yj = pt_yj.fit_transform(data)
print("Yeo-Johnson:", data_yj.flatten())
避坑指南
最后分享几个我踩过的坑:
- 先拆分再变换——一定要先用训练集拟合scaler,再用训练集的参数去变换测试集。我曾经见过有人对整个数据集做标准化再拆分,结果造成数据泄露,模型评估虚高。
- 树模型不需要特征缩放——决策树、随机森林、XGBoost这类模型对尺度不敏感,标准化反而可能降低可解释性。
- 不要盲目用Box-Cox——它虽然能自动优化参数,但有时候优化出来的λ会让数据变得难以解释。我一般先试试对数变换,效果不好再上Box-Cox。
- 注意保存scaler参数——模型上线后,新数据需要和训练数据用同样的方式变换。不保存scaler参数,上线就是灾难。
嗯,数值特征处理就这些。方法不多,但每个都有它的脾气。你想想看,选对了方法,模型效果能提升一大截;选错了,调参调到头秃也救不回来。所以,多花点时间在特征处理上,绝对值得。