因子构建基础:数据清洗与预处理、去极值与标准化、中性化处理

说实话,因子构建这事儿,80%的时间都花在数据准备上。我刚开始做量化那会儿,总觉得写策略、调参数才是核心,结果被数据坑了无数次。后来才明白——数据质量不过关,再牛的因子也是白搭。

这一章,咱们就聊聊因子构建前必须做的三件事:数据清洗与预处理去极值与标准化中性化处理。这三步走扎实了,后面的工作才能站得住脚。

因子构建预处理流程 数据清洗与预处理 缺失值·异常值·对齐 去极值与标准化 MAD法·百分位法·Z-score 中性化处理 市值·行业·回归残差 核心目标 • 消除数据噪声与异常值干扰 • 统一不同量纲因子的可比性 • 剥离已知风险暴露,提取纯净alpha信号

一、数据清洗与预处理——别让脏数据毁了你的因子

数据清洗,说白了就是给原始数据「洗澡」。我见过太多人拿到数据直接开干,结果回测漂亮得一塌糊涂,实盘一跑就崩。为什么?因为数据里有鬼。

常见的坑有这么几个:

  • 缺失值:某只股票某天停牌了,数据是NaN。你不管它,因子计算就断链。
  • 异常值:比如某只小盘股突然因为一笔错单,换手率飙到1000%。这种数据不处理,因子排名直接失真。
  • 数据对齐:不同数据源的时间戳不一致,或者复权方式没统一。我踩过这个坑——用前复权算因子,结果回测收益翻倍,实盘亏成狗。

我的处理原则:

  • 缺失值:行业均值填充,或者直接剔除(看样本量)。我个人习惯用前向填充,因为金融数据有延续性。
  • 异常值:先看业务逻辑。比如市盈率为负,是亏损还是数据错误?别一刀切。
  • 对齐:统一用后复权,时间戳精确到毫秒级。
# 一个简单的数据清洗示例(Python伪代码)
import pandas as pd
import numpy as np

# 前向填充缺失值
df['factor'] = df['factor'].fillna(method='ffill')

# 剔除市盈率为负且绝对值过大的异常值
df = df[(df['pe'] > 0) & (df['pe'] < 1000)]

# 按日期和股票代码对齐
df = df.sort_values(['date', 'code']).reset_index(drop=True)

💡 我的小技巧:每次清洗完数据,我都会画个分布图看一眼。如果因子分布突然出现「断崖」或者「尖峰」,那八成是清洗逻辑有问题。

二、去极值与标准化——让因子站在同一起跑线

数据洗干净了,接下来就是去极值和标准化。这两个操作经常一起做,但目的不同。

去极值,是为了干掉那些「离谱」的数据点。你想想看,如果某只股票的因子值是其他股票的100倍,那它在多因子模型里就会「一家独大」,其他因子全被淹没了。

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

方法 原理 适用场景
MAD法 基于中位数绝对偏差,稳健性强 因子分布有厚尾时首选
百分位法 直接截断上下1%/5%分位数 简单粗暴,适合快速验证
标准差法 超过均值±3倍标准差则截断 数据近似正态分布时好用

我个人最常用的是MAD法。为什么?因为它对异常值不敏感。标准差法容易被极端值「带偏」,而MAD法基于中位数,稳得很。

# MAD法去极值示例
def winsorize_mad(series, n=5):
    median = series.median()
    mad = (series - median).abs().median()
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

# 百分位法去极值
def winsorize_percentile(series, lower=0.01, upper=0.99):
    return series.clip(
        series.quantile(lower),
        series.quantile(upper)
    )

标准化,则是把不同量纲的因子拉到同一个尺度。比如市盈率是几十倍,换手率是百分之几,不标准化的话,模型会「偏爱」数值大的因子。

最常见的标准化是Z-score:减去均值,除以标准差。这样处理后,因子均值为0,标准差为1。

⚠️ 我曾经踩过的坑:标准化一定要在截面(同一时间点)上做,而不是在时序上做。如果你把某只股票过去一年的因子做标准化,那相当于把历史信息泄露到了当前截面——回测会严重过拟合。

# 截面标准化(按日期分组)
def standardize_cross_section(df, factor_col):
    df['zscore'] = df.groupby('date')[factor_col].transform(
        lambda x: (x - x.mean()) / x.std()
    )
    return df

三、中性化处理——剥离已知风险,提取纯净alpha

中性化处理,是因子构建中最「高级」的一步。说白了,就是把你因子里的「杂质」去掉。

举个例子:你发现小市值股票的某个因子表现特别好。但问题是,小市值本身就是一个强因子。你怎么知道你的因子是独立贡献,还是沾了小市值的光?

中性化就是干这个的——把因子对市值、行业等已知风险因素的暴露「回归掉」,剩下的残差才是你真正的alpha信号。

具体做法:

  1. 把因子值作为因变量,市值、行业等作为自变量,做截面回归。
  2. 取回归的残差作为中性化后的因子。
  3. 残差 = 原始因子 - 市值效应 - 行业效应 - ...
# 中性化处理示例(市值+行业)
import statsmodels.api as sm

def neutralize_factor(df, factor_col, market_cap_col, industry_dummies):
    # 对每个日期做截面回归
    def regress(group):
        y = group[factor_col]
        X = group[[market_cap_col] + industry_dummies]
        X = sm.add_constant(X)
        model = sm.OLS(y, X).fit()
        group['neutralized'] = model.resid
        return group
    
    df = df.groupby('date').apply(regress)
    return df

中性化之后,你的因子就和市值、行业不相关了。这时候你再去做多因子组合,才能保证每个因子都在「各司其职」。

💡 我的经验:中性化不是越多越好。有些人恨不得把100个变量都回归掉,结果因子残差变成了白噪声。我一般只中性化市值和行业,最多再加个波动率。记住,中性化的目的是「去噪」,不是「去信号」。

小结一下

数据清洗、去极值标准化、中性化处理,这三步是因子构建的「地基」。地基不稳,楼盖得再高也得塌。

我见过太多人跳过这些步骤,直接上机器学习模型。结果模型在训练集上跑得飞起,一到验证集就崩。为什么?因为数据里的异常值和噪声被模型「记住」了,而不是「学会」了。

所以,别嫌这些步骤麻烦。你花在数据预处理上的每一分钟,都会在后面的建模和实盘中加倍回报给你。


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