第四章:多因子数据预处理——去极值、标准化与中性化

数据预处理,说白了就是给原始数据“洗澡”。

我刚开始做量化那会儿,拿到数据就直接往模型里塞。结果呢?回测曲线漂亮得不行,实盘一跑就崩。后来才发现,是几个极端值把模型带偏了。嗯,从那以后,我再也不敢跳过预处理这步了。

今天咱们聊聊三个核心步骤:去极值标准化中性化。这三步做好了,因子数据才能“干干净净”地进模型。

多因子数据预处理流程 原始因子数据 第一步:去极值 MAD法 | 百分位法 第二步:标准化 Z-score | Min-Max 第三步:中性化 行业中性化 | 市值中性化 干净因子数据 处理顺序不可颠倒 先除极端值 → 再统一尺度 → 最后消除干扰

一、去极值:别让“异常值”毁了你的模型

你想想看,如果某个股票突然因为乌龙指,收益率拉了100倍。这个数据进模型,模型会怎么想?它会觉得“哇,这个因子好厉害”,然后拼命给这个股票配权重。结果呢?下一期它就跌回去了。

去极值,就是把这些“离谱”的数据拉回来。我个人习惯用两种方法:

1.1 MAD法(绝对中位差法)

MAD法比标准差法更稳健。为什么?因为标准差会被极端值本身影响,而中位数不会。我曾在处理A股小市值因子时遇到过,标准差法把正常值都砍掉了,换成MAD法就没事。

具体做法:

  • 计算因子值的中位数 median
  • 计算每个值与中位数的绝对偏差 |x - median|
  • 取这些偏差的中位数,得到 MAD
  • 设定阈值(通常3倍或5倍MAD),超出则替换为边界值
import numpy as np

def mad_winsorize(series, n=5):
    """
    MAD法去极值
    n: 阈值倍数,我一般用5
    """
    median = np.median(series)
    mad = np.median(np.abs(series - median))
    upper = median + n * mad
    lower = median - n * mad
    return np.clip(series, lower, upper)

# 举个例子
data = np.array([1,2,3,4,5,100])
clean = mad_winsorize(data)
print(clean)  # 100被拉回到上边界附近
💡 我的经验:MAD法的n值,A股数据我一般取5。如果因子本身波动大(比如换手率),可以放宽到7。别死守3倍,那是正态分布的理论值,金融数据可不长那样。

1.2 百分位法

百分位法更直观:直接砍掉头尾一定比例的数据。比如我设定1%和99%分位数,超出部分就替换成边界值。

def percentile_winsorize(series, lower=0.01, upper=0.99):
    """
    百分位法去极值
    """
    lo = np.percentile(series, lower * 100)
    hi = np.percentile(series, upper * 100)
    return np.clip(series, lo, hi)
⚠️ 注意:百分位法有个坑——如果数据量太小(比如不到100个样本),分位数估计会很不稳定。我曾经在某个小行业里用百分位法,结果上下边界几乎重合,把正常数据都削没了。所以数据量少于500时,我优先用MAD法。

二、标准化:让不同因子站在同一起跑线

去完极值,数据还是“各说各话”。有的因子在0~1之间,有的在-1000~1000之间。模型会天然偏向数值大的因子——这不公平。

标准化就是解决这个问题的。我常用两种:

2.1 Z-score标准化

把数据变成均值为0、标准差为1的分布。公式很简单:z = (x - mean) / std

def zscore_normalize(series):
    """
    Z-score标准化
    注意:要去完极值再用
    """
    return (series - np.mean(series)) / np.std(series)

Z-score的优点是保留了数据的相对大小关系。缺点是它假设数据大致对称——如果偏态严重,标准化后还是会有问题。

2.2 Min-Max标准化

把数据压缩到[0,1]区间:scaled = (x - min) / (max - min)

def minmax_normalize(series):
    """
    Min-Max标准化
    """
    return (series - np.min(series)) / (np.max(series) - np.min(series))

什么时候用哪个?

  • Z-score:适合大多数情况,尤其是后续要做回归或相关性分析时
  • Min-Max:适合因子值有明确上下界,或者你要用神经网络时(激活函数喜欢0~1输入)

我个人习惯:做多因子选股时用Z-score,做深度学习因子时用Min-Max。

三、中性化:剔除“不该有的”干扰

这一步很多人会忽略,但我觉得它最关键。

你想想看,如果一个因子在银行股上普遍高,在科技股上普遍低。那这个因子选出来的股票,到底是因子本身好,还是因为行业暴露?

中性化就是把这些“系统性偏差”剔除掉。主要做两种:

3.1 行业中性化

把因子值减去行业均值。这样每个行业内部的因子值就“公平”了。

def industry_neutralize(df, factor_col, industry_col):
    """
    行业中性化:减去行业均值
    df: 包含因子和行业标签的DataFrame
    """
    means = df.groupby(industry_col)[factor_col].transform('mean')
    return df[factor_col] - means

3.2 市值中性化

市值对因子的影响也很大。大市值股票和小市值股票,很多因子的分布都不一样。

做法是用因子值对市值做回归,取残差作为中性化后的因子:

import statsmodels.api as sm

def market_cap_neutralize(factor, log_market_cap):
    """
    市值中性化:回归取残差
    """
    X = sm.add_constant(log_market_cap)
    model = sm.OLS(factor, X).fit()
    return model.resid
💡 避坑指南:我曾经犯过一个错——先做行业中性化,再做市值中性化。结果发现两个步骤有重叠,效果反而变差。后来我改成同时做:把行业哑变量和市值一起放进回归模型,取残差。一步到位,干净利落。

四、完整的预处理流水线

把上面三步串起来,就是我的日常操作:

def preprocess_pipeline(factor_series, industry_labels, log_mkt_cap):
    """
    完整预处理流水线
    1. MAD法去极值
    2. Z-score标准化
    3. 行业+市值联合中性化
    """
    # 第一步:去极值
    clean = mad_winsorize(factor_series, n=5)
    
    # 第二步:标准化
    scaled = zscore_normalize(clean)
    
    # 第三步:中性化(联合回归)
    X = pd.get_dummies(industry_labels, drop_first=True)
    X['log_mkt_cap'] = log_mkt_cap
    X = sm.add_constant(X)
    model = sm.OLS(scaled, X).fit()
    neutralized = model.resid
    
    return neutralized

记住这个顺序:

去极值 → 标准化 → 中性化

顺序不能乱。先中性化再去极值,会把中性化后的残差又搞出极端值。先标准化再去极值,标准化本身会被极端值带偏。

嗯,这个顺序是我踩过坑之后才确定的。

五、实战中的几个小细节

步骤 常见问题 我的建议
去极值 阈值设太严,好数据被砍 先用可视化看分布,再定阈值
标准化 忘记保存均值和标准差 训练集和测试集要用同一套参数
中性化 行业分类太细或太粗 A股用申万一级行业,28个刚刚好

最后说一句:预处理不是“做完就完事”的。每次换数据、换时间窗口,都要重新跑一遍。我见过有人把去年的标准化参数直接用到今年——结果今年市场风格变了,因子全废了。

数据预处理,说白了就是给因子“卸妆”。卸干净了,模型才能看到真实的脸。


蓝海数据掘金营,专注资料整理