3、特征工程基础:单因子标准化、中性化处理、正交化方法(PCA、Gram-Schmidt)

各位好,今天我们聊点实在的。

因子挖掘出来之后,能不能直接用?不能。我早年刚入行时,把一堆原始因子直接扔进模型,结果回测曲线漂亮得不像话,一上实盘就崩。后来才发现,因子之间量纲不同、相关性太高、还有各种行业和市值暴露。说白了,你不做特征工程,模型学到的全是噪音。

这一章,我就把三个最基础也最关键的预处理手段讲清楚:标准化中性化正交化。这三板斧用好了,因子质量能上一个台阶。

3.1 单因子标准化:让因子站在同一起跑线

标准化,说白了就是把不同量纲的因子拉到同一个尺度上。比如一个因子取值在0.01~0.05,另一个在100~1000,模型会天然偏向数值大的那个——这不合理。

我个人习惯用两种方法:

  • Z-score 标准化:减去均值,除以标准差。适用于因子近似正态分布的场景。
  • Rank 标准化:把因子值映射到0~1之间的分位数。对异常值更鲁棒。

举个例子,假设我们有一个因子 factor_a

import pandas as pd
import numpy as np

# 原始因子
factor = pd.Series([1.2, 3.4, 5.6, 100.0, 2.3])

# Z-score 标准化
z_score = (factor - factor.mean()) / factor.std()

# Rank 标准化
rank = factor.rank(pct=True)

print("Z-score:", z_score.tolist())
print("Rank:", rank.tolist())
我的经验:如果因子尾部有极端值,Z-score会把正常值压缩到很小范围。这时候我更喜欢Rank标准化。我在一个CTA策略里遇到过,一个因子因为某天数据异常,Z-score后所有信号都失效了——换成Rank就稳了。

3.2 中性化处理:剔除你不想要的暴露

因子常常跟某些风险因子(比如市值、行业)高度相关。举个例子,你发现一个因子在小盘股上表现特别好,但这不是因子本身的能力,而是小盘股风格在起作用。中性化就是把这些“脏东西”剥离掉。

怎么做?回归取残差

把因子作为因变量,把你要中性化的变量(比如市值、行业哑变量)作为自变量,做线性回归。残差就是中性化后的因子。

import statsmodels.api as sm

# 假设我们有因子值和市值
factor = np.array([1.2, 3.4, 5.6, 2.3, 4.5])
market_cap = np.array([10, 100, 50, 200, 30])

# 加截距项
X = sm.add_constant(market_cap)
model = sm.OLS(factor, X).fit()
residual = model.resid

print("中性化后的因子:", residual)
注意:中性化不是万能的。我曾经把行业中性化做得太狠,结果因子在所有行业里都变得不显著了——因为因子本身的信息有一部分就来自行业配置。所以,中性化之前,先想清楚你到底要保留什么、剔除什么。

3.3 正交化方法:让因子之间不再“打架”

多个因子之间如果高度相关,模型会不稳定,甚至出现多重共线性。正交化就是让因子之间相互独立。

这里介绍两种主流方法:PCAGram-Schmidt

3.3.1 PCA:主成分分析

PCA 把原始因子投影到一组正交的主成分上。这些主成分彼此不相关,而且按方差大小排序。我一般用 PCA 做降维——比如你有50个因子,可能前10个主成分就解释了90%的方差。

from sklearn.decomposition import PCA

# 假设我们有3个因子,100个样本
np.random.seed(42)
factors = np.random.randn(100, 3)

pca = PCA(n_components=3)
pca_factors = pca.fit_transform(factors)

print("主成分方差解释比例:", pca.explained_variance_ratio_)
print("正交化后的因子形状:", pca_factors.shape)
关键点:PCA 后的因子失去了原始含义。你没法说“主成分1代表动量因子”——它只是数学上的组合。所以,如果你需要可解释性,PCA 可能不是最佳选择。

3.3.2 Gram-Schmidt:顺序正交化

Gram-Schmidt 过程是逐次正交化。你先选一个基准因子,然后把后续因子依次投影到前面因子的正交补空间上。这样做的好处是:你可以控制正交化的顺序

比如,你有一个你非常信任的因子 F1,你想让其他因子都跟它正交,但保留 F1 本身不变。Gram-Schmidt 就能做到。

def gram_schmidt(factors):
    """
    factors: (n_samples, n_factors) 的 numpy 数组
    返回正交化后的因子矩阵
    """
    n_samples, n_factors = factors.shape
    ortho = np.zeros_like(factors)
    
    for i in range(n_factors):
        v = factors[:, i].copy()
        for j in range(i):
            # 减去在前一个因子上的投影
            proj = np.dot(v, ortho[:, j]) / np.dot(ortho[:, j], ortho[:, j])
            v -= proj * ortho[:, j]
        ortho[:, i] = v
    
    return ortho

# 示例
factors = np.array([[1, 2, 3],
                    [4, 5, 6],
                    [7, 8, 9]], dtype=float)
ortho_factors = gram_schmidt(factors)
print("正交化后的因子:\n", ortho_factors)
我的习惯:如果因子数量不多(比如5~10个),我倾向于用 Gram-Schmidt,因为我可以控制顺序。比如先把最稳定的因子放前面,把噪音大的放后面。PCA 适合因子数量多、且你不在乎解释性的场景。

3.4 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,从原始因子到最终可用因子,中间经历了三个关键步骤:

因子预处理流程 原始因子 标准化 中性化 正交化 原始因子值 不同量纲 Z-score / Rank 统一尺度 回归取残差 剔除风险暴露 PCA / Gram-Schmidt 消除相关性 最终目标:干净、独立、可解释的因子 标准化保证量纲一致 → 中性化剔除风险暴露 → 正交化消除多重共线性

3.5 实际应用中的顺序问题

这三个步骤的顺序有讲究吗?

我个人建议的顺序是:先标准化,再中性化,最后正交化

  • 标准化放在最前面,因为中性化和正交化都依赖于数值的尺度一致性。
  • 中性化放在正交化之前,因为中性化本质上是回归,回归对因子间的相关性有一定容忍度。
  • 正交化放在最后,因为它是“最终清洗”,确保因子之间彻底独立。
避坑指南:我曾经试过先正交化再中性化,结果发现中性化后的因子又产生了新的相关性——因为中性化过程本身会改变因子结构。所以,顺序别搞反了。

好了,这一章的内容就到这里。标准化、中性化、正交化,这三步是因子预处理的核心。你想想看,如果连这些基础都没做好,后面再复杂的模型也是白搭。下一章我们会聊到因子合成与筛选,到时候这些预处理后的因子就能派上大用场了。


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