第四节:因子预处理——去极值、标准化、中性化

各位好,我是老张。今天咱们聊聊因子预处理。

说实话,很多做量化的人容易忽略这一步。他们觉得,因子算出来,直接扔进模型就完事了。我刚开始也是这么干的,结果呢?回测曲线漂亮得不行,一到实盘就崩。后来我才明白,问题出在因子预处理上。

因子预处理,说白了就是给原始因子数据「洗个澡」。把脏东西洗掉,把数据弄整齐,让模型能好好干活。今天咱们重点讲三个步骤:去极值、标准化、中性化。

核心观点:因子预处理不是可选项,而是必选项。不做预处理的因子,就像没洗过的菜,你敢直接下锅吗?

一、去极值:把「妖怪」揪出来

什么叫极值?就是那些离谱的数据点。比如某个股票的市盈率突然变成10000倍,或者某个因子值比平均值高出几十个标准差。这些数据点,说白了就是「妖怪」。

我在项目中遇到过这样的情况:一个因子本来选股效果不错,但因为某只小盘股的数据异常,导致整个因子排名被带偏。后来一查,是数据源的问题。嗯,从那以后,去极值就成了我的标准流程。

常用的去极值方法有三种:

  • MAD法(中位数绝对偏差法):我个人最推荐的方法。它用中位数代替均值,对异常值更鲁棒。
  • 百分位法:直接砍掉上下1%或5%的数据。简单粗暴,但有效。
  • 标准差法:超过3倍标准差的数据被截断。这个方法对正态分布的数据效果好,但金融数据往往不是正态的。

给你看个代码示例,我平时用的MAD法:

import numpy as np
import pandas as pd

def winsorize_mad(factor, n=5):
    """
    MAD法去极值
    factor: 因子值序列
    n: 阈值倍数,默认5倍MAD
    """
    median = np.median(factor)
    mad = np.median(np.abs(factor - median))
    upper = median + n * mad
    lower = median - n * mad
    
    # 截断处理
    factor_clipped = np.clip(factor, lower, upper)
    return factor_clipped

# 使用示例
factor_series = pd.Series([1.2, 1.5, 1.3, 100.0, 1.4, 1.1, 1.6])
cleaned = winsorize_mad(factor_series)
print(cleaned)
# 输出:1.2, 1.5, 1.3, 截断值, 1.4, 1.1, 1.6

小技巧:MAD法的阈值n,我一般取5。如果数据噪声大,可以降到3。但别太激进,否则会把有效信息也砍掉。

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

标准化,就是把不同量纲的因子拉到同一个尺度上。你想想看,市盈率可能是几十,市净率可能是几,换手率可能是百分之几。如果不做标准化,模型会天然偏向数值大的因子。

常用的标准化方法:

方法 公式 适用场景
Z-score标准化 (x - mean) / std 数据近似正态分布时
中位数标准化 (x - median) / mad 数据有异常值时
Rank标准化 排序后映射到[0,1] 因子值分布不规则时

我个人习惯用Z-score标准化,但前提是已经做过去极值。否则,一个异常值就能把均值和标准差带偏,标准化就白做了。

def standardize_zscore(factor):
    """
    Z-score标准化
    """
    mean = np.mean(factor)
    std = np.std(factor)
    factor_std = (factor - mean) / std
    return factor_std

# 注意:标准化前一定要去极值!
factor_cleaned = winsorize_mad(factor_series)
factor_standardized = standardize_zscore(factor_cleaned)

避坑指南:我曾经犯过一个错误——在截面数据上做标准化时,忘了按时间分组。结果把不同时间点的数据混在一起标准化,导致因子失效。记住:标准化一定要在每个时间截面上独立进行。

三、中性化:剔除「噪音」干扰

中性化,是因子预处理中最关键也最容易被忽视的一步。它的目的是剔除因子中与市值、行业等因素相关的部分。

为什么要这么做?举个例子:小市值股票天然有更高的收益。如果你的因子恰好在小市值股票上表现好,那到底是因子有效,还是小市值效应在起作用?

中性化的核心思路:用回归把「噪音」部分剥离掉。

import statsmodels.api as sm

def neutralize_factor(factor, market_cap, industry_dummies):
    """
    因子中性化处理
    factor: 待中性化的因子值
    market_cap: 市值(取对数)
    industry_dummies: 行业哑变量矩阵
    """
    # 构建回归模型
    X = pd.concat([market_cap, industry_dummies], axis=1)
    X = sm.add_constant(X)
    y = factor
    
    # 回归
    model = sm.OLS(y, X).fit()
    residuals = model.resid
    
    # 残差就是中性化后的因子
    return residuals

这段代码的逻辑很简单:把因子对市值和行业做回归,取残差。残差就是剔除掉市值和行业影响后的「纯净」因子。

关键点:中性化后的因子,与市值和行业的相关性接近于0。这样我们才能确认,因子的选股能力来自因子本身,而不是来自市值或行业暴露。

四、知识体系总览

说了这么多,咱们用一张图来总结一下因子预处理的完整流程:

因子预处理流程 原始因子数据 步骤1:去极值 步骤2:标准化 步骤3:中性化 MAD法 / 百分位法 Z-score / Rank标准化 回归残差法 剔除市值、行业影响 顺序不能乱:先去极值 → 再标准化 → 最后中性化

五、实战中的注意事项

最后,分享几个我在实战中踩过的坑:

  1. 顺序问题:先做去极值,再做标准化,最后做中性化。顺序乱了,效果会打折扣。
  2. 分组处理:每个时间截面独立处理,不要把不同时间的数据混在一起。
  3. 行业分类:中性化时用的行业分类要统一。我见过有人用申万一级行业做中性化,但因子计算时用的是中信行业,结果对不上。
  4. 市值处理:市值一般取对数后再做中性化,因为市值分布是偏态的。

我的习惯:每次做完因子预处理,我都会检查一下因子与市值、行业的相关性。如果相关性还很高,说明中性化没做好,需要重新调整。

好了,因子预处理就讲到这里。记住:预处理做得好,因子选股差不了。别嫌麻烦,这一步值得花时间。


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