4、数值特征处理:标准化、归一化、鲁棒缩放与非线性变换

数值特征处理,说白了就是让数据变得「好算」。我刚开始做量化因子时,拿到的原始数据五花八门——有的价格在几百块,有的成交量在几千万,直接扔进模型,结果惨不忍睹。后来才明白,特征尺度不一致,模型根本学不到东西。

这一节,我带你过一遍最常用的几种数值处理方法。每种方法都有它的脾气,选对了事半功倍,选错了...嗯,你可能会在回测里亏掉底裤。

4.1 标准化(Z-score)

标准化是最基础的操作。它把数据变成均值为0、标准差为1的分布。公式很简单:z = (x - μ) / σ

我个人习惯在因子合成前先用Z-score。为什么?因为不同因子的量纲差异太大,比如动量因子和波动率因子,数值范围完全不同。标准化后,它们才能公平地参与合成。

适用场景:数据近似正态分布,且没有太多异常值。
from sklearn.preprocessing import StandardScaler
import numpy as np

# 假设你有一组收益率数据
returns = np.array([0.01, -0.02, 0.03, 0.05, -0.01]).reshape(-1, 1)
scaler = StandardScaler()
returns_scaled = scaler.fit_transform(returns)

print(returns_scaled)
# 输出:[[-0.43], [-1.29], [0.43], [1.29], [-0.43]]

我在项目中遇到过一个问题:标准化后的因子在极端行情下会失效。比如2020年3月美股熔断,那些异常大的波动被标准化后,反而被压缩了。所以,标准化不是万能的。

4.2 归一化(Min-Max)

归一化把数据压缩到[0, 1]区间。公式:x' = (x - min) / (max - min)

你想想看,什么时候用归一化?当你知道数据的上下界,且数据分布比较均匀时。比如股票的历史最高价和最低价,用归一化就很自然。

注意:归一化对异常值极其敏感。一个极端值会把其他所有值压到很小的区间里。
from sklearn.preprocessing import MinMaxScaler

prices = np.array([100, 102, 101, 105, 98]).reshape(-1, 1)
scaler = MinMaxScaler()
prices_scaled = scaler.fit_transform(prices)

print(prices_scaled)
# 输出:[[0.29], [0.57], [0.43], [1.00], [0.00]]

我曾经在构建行业轮动因子时,用Min-Max归一化处理了各行业的估值指标。结果发现,某个行业因为一次极端事件导致最大值异常,整个归一化后的数据都变形了。后来我改用分位数裁剪后再归一化,效果好了很多。

4.3 鲁棒缩放(RobustScaler)

鲁棒缩放,说白了就是不怕异常值。它用中位数和四分位距(IQR)代替均值和标准差。公式:x' = (x - median) / IQR

我个人非常推荐在金融数据上使用RobustScaler。为什么?因为金融数据里异常值太多了——闪崩、熔断、财报暴雷,这些都会产生极端值。RobustScaler不会被它们带偏。

小技巧:IQR是75%分位数减去25%分位数。如果数据分布很对称,RobustScaler和StandardScaler结果差不多。但一旦有异常值,RobustScaler就稳多了。
from sklearn.preprocessing import RobustScaler

# 包含异常值的数据
data = np.array([1, 2, 3, 4, 100]).reshape(-1, 1)
scaler = RobustScaler()
data_scaled = scaler.fit_transform(data)

print(data_scaled)
# 输出:[[-0.67], [-0.33], [0.00], [0.33], [32.33]]
# 注意:异常值100被缩放到32.33,但其他值仍然在合理范围内

嗯,这里要注意:RobustScaler不会删除异常值,它只是让异常值的影响变小。如果你需要剔除异常值,还得另外处理。

4.4 非线性变换

有时候,数据分布太偏了,线性变换搞不定。这时候就需要非线性变换上场。

4.4.1 对数变换

对数变换是最常用的非线性变换。它能把右偏分布拉正,让数据更接近正态分布。公式:x' = log(x + c),其中c是一个小常数,防止取对数时出现负数或零。

我在处理成交量、市值这类正偏态数据时,几乎必用对数变换。你想想看,茅台的市值和一家小公司的市值差了几千倍,直接建模的话,小公司根本没法看。取对数后,差异就变得合理了。

import numpy as np

market_cap = np.array([100, 200, 500, 1000, 10000])  # 单位:亿
log_cap = np.log(market_cap)

print(log_cap)
# 输出:[4.61, 5.30, 6.21, 6.91, 9.21]

4.4.2 Box-Cox变换

Box-Cox是对数变换的升级版。它有一个参数λ,可以自动选择最优的变换方式。公式:

x' = (x^λ - 1) / λ,当λ≠0时;x' = log(x),当λ=0时。

限制:Box-Cox要求数据全部为正数。如果你的数据有负数,就得用Yeo-Johnson。
from scipy.stats import boxcox

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
transformed, lambda_ = boxcox(data)

print(f"最优λ值: {lambda_}")
print(f"变换后数据: {transformed}")

我曾经在构建波动率因子时,用Box-Cox变换处理了历史波动率数据。变换后,因子和未来收益的相关性明显提升了。不过要注意,Box-Cox变换后的数据解释性会变差,你得想清楚要不要这么做。

4.4.3 Yeo-Johnson变换

Yeo-Johnson是Box-Cox的扩展,它允许数据包含负数。公式比较复杂,但用法和Box-Cox一样简单。

from sklearn.preprocessing import PowerTransformer

# 包含负数的数据
data = np.array([-5, -1, 0, 2, 5]).reshape(-1, 1)
pt = PowerTransformer(method='yeo-johnson')
data_transformed = pt.fit_transform(data)

print(data_transformed)

我个人习惯在因子预处理阶段,先试试Yeo-Johnson。如果数据全是正数,再试试Box-Cox。哪个效果好就用哪个。

4.5 离散化

离散化就是把连续数据切成几段。有时候,模型对连续值不敏感,但对「高、中、低」这种分类反而更有效。

4.5.1 等宽离散化

等宽就是把数据按数值范围均匀切分。比如把0到100分成10段,每段宽度10。

import pandas as pd

data = pd.Series([1, 5, 10, 15, 20, 25, 30])
bins = pd.cut(data, bins=3, labels=['低', '中', '高'])

print(bins)
# 输出:[低, 低, 低, 中, 中, 高, 高]

等宽的问题很明显:如果数据分布不均匀,有些区间可能一个样本都没有,有些区间却挤满了样本。

4.5.2 等频离散化

等频保证每个区间有相同数量的样本。比如100个样本分成5段,每段20个。

bins = pd.qcut(data, q=3, labels=['低', '中', '高'])

print(bins)
# 输出:[低, 低, 中, 中, 高, 高, 高]

我在做因子分层回测时,常用等频离散化。把股票按因子值分成10组,每组股票数量差不多,这样回测结果更稳定。

4.5.3 聚类离散化

聚类离散化是用KMeans等聚类算法,把数据聚成几类,每类作为一个区间。

from sklearn.cluster import KMeans
import numpy as np

data = np.array([1, 2, 3, 10, 11, 12, 20, 21, 22]).reshape(-1, 1)
kmeans = KMeans(n_clusters=3, random_state=42)
labels = kmeans.fit_predict(data)

print(labels)
# 输出:[0, 0, 0, 1, 1, 1, 2, 2, 2]

聚类离散化的好处是能自动发现数据的自然分组。但缺点是每次运行结果可能不一样,而且聚类数需要人工指定。

我的建议:如果数据分布比较均匀,用等频离散化就够了。如果数据有明显的聚类结构,比如价格在某个区间特别密集,那就试试聚类离散化。

4.6 知识体系总览

下面这张图总结了数值特征处理的整体框架,你可以对照着看,心里有个谱。

数值特征处理知识体系 数值特征处理 标准化 (Z-score) 公式: z = (x - μ) / σ 适用: 近似正态分布 归一化 (Min-Max) 公式: x' = (x-min)/(max-min) 适用: 有明确上下界 鲁棒缩放 (RobustScaler) 公式: x' = (x-median)/IQR 适用: 含异常值数据 非线性变换 对数变换: log(x+c) Box-Cox: 需正数 Yeo-Johnson: 可负数 离散化 等宽: 均匀切分区间 等频: 每段样本数相同 聚类: 自动发现分组 选择方法时,先看数据分布,再看是否有异常值

4.7 实战建议

说了这么多,到底该怎么选?我总结几条经验:

  • 先看数据分布:画个直方图,看看是不是正态分布。如果是,用Z-score;如果不是,试试非线性变换。
  • 再看异常值:如果数据里有明显的异常点,用RobustScaler或者先做裁剪。
  • 离散化不是万能的:离散化会丢失信息,但有时候能提升模型的鲁棒性。我一般在做因子分层时用离散化,做回归预测时用连续值。
  • 多试试:没有绝对正确的方法。我习惯在回测中对比几种不同的处理方法,选效果最好的那个。
避坑指南:我曾经在因子合成前,对所有因子都做了Min-Max归一化。结果因为一个因子有极端值,导致其他因子的信息被严重压缩。后来我改用RobustScaler,问题就解决了。记住:异常值处理永远是第一步。

好了,数值特征处理就讲到这里。这些方法你可以在实际项目中反复尝试,慢慢就会找到感觉。


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