【维克】特征归一化与标准化:为什么模型对数据的“尺度“很敏感?

我第一次训练机器学习模型的时候,信心爆棚。

花了三天选因子。量价因子、基本面因子、技术面因子,整整选了12个。

市盈率、市净率、换手率、日成交量、20日波动率、RSI、MACD柱......

每个都有理论支撑。每个跑单因子IC的时候都显著。

我把它们一股脑扔进模型。线性回归。

跑完回测一看------

夏普0.3。

亏了。

我以为是模型不行。换了XGBoost。

夏普0.2。

又亏了。

我以为是因子不行。一个一个剔除,换新的。折腾了一周。

夏普还是0.3。

导师路过,瞟了一眼我的代码。

"你归一化了没有?"

"归什么?"

"特征缩放。你把市盈率0到80的数值,和成交量几亿到几十亿的数值,直接一起喂给模型了?"

我点头。

"那模型当然学不到东西。"

他拉过来键盘,加了四行代码。

// python
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

四行。

重新跑。

夏普------1.2。

我盯着屏幕,嘴巴张了五分钟没合上。

四行代码。从0.3到1.2。

我浪费了一整周,问题出在------

特征没有做归一化。

WHY:为什么模型这么"以貌取数"?

一个生活中的比喻

想象你要给两个同学做综合排名。

同学A:语文考了85分(满分100)。

同学B:数学考了9分(满分10)。

如果你直接比原始分数------

85 vs 9。

85远大于9。同学A碾压同学B。

但这公平吗?

不公平。

同学B的9分是满分,换算成百分制是90分,比同学A的85分还高。

问题出在------

两个分数的"尺度"不一样。

一个是百分制。一个是十分制。

直接比大小,毫无意义。

模型也是这个逻辑。

模型不认识你的数据。它不知道"市盈率30"代表高估值,也不知道"日成交量5亿"代表放量。

它只认识数字。

当你的特征A范围是0到80,特征B范围是1亿到50亿------

模型在计算梯度的时候,特征B的数值比特征A大了九个数量级。

梯度被特征B完全主导。

特征A的信息------不管它多有预测力------被模型直接忽略了。

这就好比------

你跟一个体重200斤的人比体重。

你跟一个身高1米9的人比身高。

然后你说:我们来算"综合身体素质"。

体重200斤直接碾压一切。身高1米9也碾压。

但你真正关心的------心肺功能、反应速度、柔韧性------

全被忽略了。

因为它们和体重、身高不在一个量级。

模型也一样。

不做特征缩放,就是在让大数值的特征欺负小數值的特征。

所以------特征缩放,本质上是在给模型建一个"公平考场"。

让每个特征都在同一个尺度上竞争。

模型才能公平地评估每一个因子的预测力。

WHAT:三种主流的"公平考场"

特征缩放,主流三种方法。

每种方法解决不同的问题。

一个一个来。

第一种:Min-Max归一化------把数据"压"到0到1之间

最直观。

把每个特征的最小值映射到0,最大值映射到1,其他值按比例排在中间。

就像考试成绩归一化------

全班最低分映射成0。最高分映射成1。其他同学按比例排。

公式也简单:

Xscaled = (X - X min) / (Xmax - X min)

// python
def min_max_normalize(values):
"""Min-Max归一化:将数据缩放到0, 1区间"""
min_val = values.min()
max_val = values.max()
return (values - min_val) / (max_val - min_val)

示例

pe_ratio = pd.Series(5, 10, 20, 35, 80) # 市盈率:5到80
volume = pd.Series(1e8, 5e8, 2e9, 1e10) # 成交量:1亿到100亿

pe_norm = min_max_normalize(pe_ratio)
vol_norm = min_max_normalize(volume)

print("市盈率归一化后:", pe_norm.values)
print("成交量归一化后:", vol_norm.values)

两组数据现在都在0到1之间,模型公平对待

输出:

市盈率归一化后: 0. 0.067 0.2 0.4 1.
成交量归一化后: 0. 0.04 0.19 1.

两组数据现在都在0到1之间。模型可以公平对待了。

但------有一个大坑。

Min-Max对异常值极其敏感。

假设你的成交量里有一个值:500亿。

X_max 直接变成500亿。

其他正常的1亿到10亿------全部被压缩到0附近。

0.002、0.004、0.02......

挤在一起。模型完全分不清差异。

一个异常值,毁了所有正常数据的区分度。

所以Min-Max适合------数据分布稳定、没有极端异常值的场景。

比如:技术指标(RSI、MACD),它们的值域本身就有天然边界。

第二种:Z-score标准化------用"标准差"做尺子

Min-Max的问题是太在意极值。

Z-score换了个思路------不看你在全局的绝对位置,看你离平均值有几个标准差。

打个比方:

全班身高,平均值170cm,标准差5cm。

你身高180cm------比平均值高2个标准差------标准化后你的值是+2。

你身高165cm------比平均值低1个标准差------标准化后你的值是-1。

你身高170cm------正好是平均值------标准化后你的值是0。

这样,不管原始数据的尺度是厘米还是毫米,标准化之后都是"离平均值几个标准差"。

公式:

Xscaled = (X - X mean) / X_std

// python
def z_score_standardize(values):
"""Z-score标准化:均值为0,标准差为1"""
mean_val = values.mean()
std_val = values.std()
return (values - mean_val) / std_val

示例

pe_std = z_score_standardize(pe_ratio)
vol_std = z_score_standardize(volume)

print("市盈率标准化后:", pe_std.values.round(3))
print("成交量标准化后:", vol_std.values.round(3))

输出:

市盈率标准化后: -1.092 -0.79 -0.189 0.711 1.369
成交量标准化后: -0.816 -0.665 -0.254 1.735

均值变成0,标准差变成1。

和Min-Max比------Z-score的好处是:不依赖极值

一个异常值会让均值和标准差稍微偏移一点,但不会像Min-Max那样把正常数据全部压到0附近。

适用场景:

大多数机器学习模型------神经网络、SVM、KNN、逻辑回归------Z-score是默认选择。

如果不确定用哪个------先用Z-score。

第三种:RobustScaler------异常值?直接无视

金融数据有个特点------异常值特别多。

市场崩盘那天,成交量是平时的十倍。

黑天鹅事件,波动率飙到天上去。

财务造假公司的市盈率是负数或者几百倍。

Z-score虽然比Min-Max稳健,但均值和标准差还是会被异常值拉偏。

怎么办?

用中位数代替均值。用四分位距(IQR)代替标准差。

中位数不受极端值影响。10个人排队,不管首富多有钱,中位数永远是第5个人的收入。

四分位距也不受极端值影响------它只看中间50%的数据范围。

// python
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()

假设有极端异常值

volume_with_outlier = pd.Series(1e8, 2e8, 3e8, 5e8, 500e8) # 最后一个500亿是异常值

X = volume_with_outlier.values.reshape(-1, 1)
vol_robust = scaler.fit_transform(X)

print("原始值(亿):", volume_with_outlier.values / 1e8)
print("RobustScaler后:", vol_robust.flatten().round(3))

500亿的异常值被压到合理范围,正常数据之间的区分度保留得很好。

三种方法怎么选?

|--------------|------------|----------|-------------------|
| 方法 | 核心逻辑 | 抗异常值 | 适用场景 |
| Min-Max | 压到0,1 | 差 | 值域有天然边界(RSI、MACD) |
| Z-score | 均值为0,标准差为1 | 中等 | 大多数ML模型(默认选择) |
| RobustScaler | 中位数+IQR | 强 | 有明显异常值的金融数据 |

量化场景的经验法则:

价格类特征(开盘价、收盘价)→ Z-score

成交量类特征 → RobustScaler(量经常有极端值)

技术指标(RSI、MACD)→ Min-Max(值域本身有界)

基本面特征(市盈率、市净率)→ RobustScaler(有极端异常值)

HOW:完整的归一化流水线

实战中不是选一种方法就够了。

你有12个特征。每个特征尺度不同。每个特征的数据分布不同。

你需要的是一个完整的流水线------

不同特征用不同的缩放方法。

// python
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, RobustScaler, MinMaxScaler

模拟因子数据

np.random.seed(42)
n = 200

df = pd.DataFrame({
'pe_ratio': np.random.uniform(5, 80, n), # 市盈率:有界
'volume': np.random.lognormal(20, 1.5, n), # 成交量:对数分布,有极端值
'rsi': np.random.uniform(0, 100, n), # RSI:天然0-100
'volatility': np.random.exponential(0.02, n), # 波动率:右偏分布
})

定义每个特征对应的缩放方法

scalers = {
'pe_ratio': RobustScaler(), # 基本面,有异常值
'volume': RobustScaler(), # 成交量,有极端值
'rsi': MinMaxScaler(), # RSI,天然有界
'volatility': StandardScaler(), # 波动率,右偏但用Z-score够用
}

逐列缩放

df_scaled = pd.DataFrame()
for col, scaler in scalers.items():
df_scaledcol = scaler.fit_transform(df\[col])

print("缩放前各特征范围:")
print(df.describe().loc\['min','max'].round(2))
print("\n缩放后各特征范围:")
print(df_scaled.describe().loc\['min','max'].round(2))

这就是一个完整的归一化流水线。

每个特征用最适合自己的缩放方法。

缩放之后------所有特征都在合理的尺度上。模型可以公平地学习每一个因子。

几个坑,必须踩了才知道

坑一:未来数据泄露

这是最常见的错误,也是最致命的错误。

// python

❌ 错误写法

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 用全部数据(包括测试集)做归一化
X_train, X_test = train_test_split(X_scaled)

✅ 正确写法

scaler = StandardScaler()
X_train, X_test = train_test_split(X) # 先分割
X_train_scaled = scaler.fit_transform(X_train) # 只用训练集fit
X_test_scaled = scaler.transform(X_test) # 测试集只用transform

为什么?

因为fit的时候会计算均值和标准差。

如果你用全部数据fit------测试集的信息就泄露到了训练集。

模型在训练时"偷看"了答案。

回测可能很好。实盘一定很烂。

坑二:树模型不需要归一化

XGBoost、LightGBM、RandomForest------基于树的模型------完全不需要特征缩放。

因为树模型是按分位数切分的。它不关心数值的绝对大小,只关心大小排序。

"成交量大于中位数"------不管中位数是1亿还是10亿,树模型都能正确切分。

所以:

用神经网络/SVM/KNN → 必须归一化

用XGBoost/LightGBM/随机森林 → 不需要归一化

我当年用XGBoost也做了归一化------虽然没坏处,但也没好处。白白浪费了时间。

坑三:交叉验证时的数据泄露

如果你做交叉验证------不要在全部数据上做归一化再分折。

应该在每一折内,只对训练部分fit,然后transform验证部分。

用Pipeline可以自动处理:

// python
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score

pipe = Pipeline( ('scaler', StandardScaler()), ('model', LinearRegression()) )

scores = cross_val_score(pipe, X, y, cv=5)

Pipeline会自动保证每一折的归一化只用训练数据。不会泄露。

坑四:实盘推理时要用训练集的scaler

训练时fit了一个scaler。实盘推理时------必须用同一个scaler。

不能重新fit。

如果你实盘时用当天的数据重新fit------同一个特征值,在不同日期会得到不同的缩放结果。

模型的输入就不稳定了。

今天预测准、明天预测不准------可能不是策略变了,而是你的scaler变了。

// python
import joblib

训练时保存scaler

joblib.dump(scaler, 'feature_scaler.pkl')

实盘时加载同一个scaler

scaler = joblib.load('feature_scaler.pkl')
X_live_scaled = scaler.transform(X_live) # transform,不是fit_transform

回到那个故事

导师加了四行代码。

夏普从0.3变成了1.2。

不是因为策略不好。

是因为特征的尺度差异太大,模型根本学不到东西。

那个下午,我学到了一条量化新手最容易忽略、也最容易犯的错------

不是你的模型不行。是你的数据没处理好。

很多人花大量时间调模型参数、选因子、换架构。

但忽略了最基础的一步------把数据缩放到合理的尺度。

这就像盖房子------

地基不平。你买再贵的砖、再好的水泥,房子也是歪的。

归一化,就是把地基找平。

平了之后,你才能在上面试各种结构、各种材料。

下一节------

特征选好了,也归一化了。但12个特征真的都需要吗?

有些特征可能是冗余的。有些可能根本没信息量。

下一篇,我们聊特征选择------怎么从12个特征里,筛出真正有用的那几个。

OVER。

相关推荐
2601_962298671 小时前
Python multiprocessing PicklingError: Can't pickle &l
python·module·multiprocessing·function·picklingerror
朝阳5811 小时前
如何用 AI 自动生成规范的 Git 提交信息:从暂存区 diff 到 Conventional Commit
人工智能·git
CV山月1 小时前
大模型强化学习对齐:从 RLHF 框架到 PPO 算法原理
人工智能·python·大模型·强化学习·多模态·研究生
Profile排查笔记1 小时前
指纹浏览器推荐:用一套验收清单筛选 Profile、代理与自动化能力
前端·人工智能·后端·自动化
CET中电技术1 小时前
预付费电表计费系统是什么?云综能Lite如何重构水电费管理?
人工智能·物联网·重构
用户739548002061 小时前
本地 CSV 清洗的小细节:先标准化,再去重,并保留可检查的报告
python
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 31
android·spring boot·笔记
GEO实战经验分享1 小时前
GEO知识投毒风险解析与多维防御策略指南
人工智能
Qforepost1 小时前
72 量子比特跑真实道路图像,量子机器学习进入“硬仗”
人工智能·机器学习·量子计算