第4章:数据清洗与预处理

数据预处理,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人拿着脏数据就跑模型,结果回测漂亮得像神话,实盘一跑就现原形。嗯,这一章我们就把数据洗干净。

4.1 缺失值处理

股票数据里缺失值太常见了。停牌、新股上市、数据源抽风,都会导致空值。我个人习惯,拿到数据第一件事就是看缺失率。

核心原则:缺失率超过50%的因子,直接扔掉。别犹豫,补了也是噪声。

4.1.1 删除法

最简单粗暴,但也最有效。适用于缺失值占比很小的情况。

import pandas as pd
import numpy as np

# 删除含有缺失值的行
df_clean = df.dropna()

# 删除缺失率超过30%的列
df_clean = df.dropna(thresh=len(df) * 0.7, axis=1)

我的经验:回测时用删除法没问题,但实盘交易时,当天缺失的数据你没法删——你得实时处理。所以实盘我一般用填充法。

4.1.2 填充法

常用的填充方式有几种:

  • 均值/中位数填充:简单,但会压缩方差
  • 前向填充:用上一个交易日的数据,适合停牌场景
  • 行业均值填充:用同行业股票的均值,我比较推荐这个
# 行业均值填充
df['pe_ratio'] = df.groupby('industry')['pe_ratio'].transform(
    lambda x: x.fillna(x.mean())
)

# 前向填充(适合停牌)
df['volume'] = df.groupby('stock_code')['volume'].ffill()

注意:千万不要用未来数据填充!我曾经见过有人用后一天的收盘价填充前一天的缺失值,回测结果漂亮得离谱,实盘直接崩了。

4.2 异常值检测

异常值就是那些离谱的数据点。比如某只股票市盈率突然变成10000倍,或者换手率飙到99%。你想想看,这明显是数据录入错误或者极端事件。

4.2.1 3σ原则

假设数据服从正态分布,超过均值±3倍标准差的就是异常值。这个方法简单,但股票数据往往不服从正态分布,所以效果一般。

def detect_outliers_3sigma(series):
    mean = series.mean()
    std = series.std()
    lower = mean - 3 * std
    upper = mean + 3 * std
    return (series < lower) | (series > upper)

4.2.2 四分位距法(IQR)

这个方法更稳健,不受极端值影响。我比较常用这个。

def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return (series < lower) | (series > upper)

避坑指南:我曾经在市值因子上用3σ法,结果把茅台这种大市值股票标记为异常值,直接删掉了。后来改用IQR法,才保留了这些真正的优质股。

4.3 数据标准化

不同因子的量纲不一样。市盈率可能是几十,换手率是百分之几,市值是几十亿。不标准化的话,模型会天然偏向数值大的因子。

4.3.1 Z-score标准化

把数据变成均值为0、标准差为1的分布。这是最常用的方法。

from scipy import stats

def zscore_standardize(series):
    return (series - series.mean()) / series.std()

# 或者直接用scipy
df['pe_zscore'] = stats.zscore(df['pe_ratio'])

4.3.2 排序标准化

把数据映射到0到1之间。这个方法对异常值不敏感,我特别喜欢在组合构建时用。

def rank_standardize(series):
    ranks = series.rank()
    return (ranks - 1) / (len(ranks) - 1)

关键点:标准化一定要在训练集上计算参数,然后应用到测试集。否则会造成数据泄露,回测结果虚高。

4.4 中性化处理

中性化,说白了就是剔除掉你不想要的「风格暴露」。比如市值因子和行业因子,很多时候我们想剥离它们的影响,只看个股自身的alpha。

4.4.1 市值中性化

把因子值对市值做回归,取残差作为新的因子值。这样新因子就和市值无关了。

import statsmodels.api as sm

def market_neutralize(factor, market_cap):
    X = sm.add_constant(market_cap)
    model = sm.OLS(factor, X).fit()
    residual = model.resid
    return residual

4.4.2 行业中性化

行业内做标准化,让每个行业的因子均值为0、标准差为1。这样不同行业之间就可以直接比较了。

def industry_neutralize(df, factor_col, industry_col):
    df['factor_neutral'] = df.groupby(industry_col)[factor_col].transform(
        lambda x: (x - x.mean()) / x.std()
    )
    return df

重要提醒:中性化不是万能的。过度中性化会抹掉真实的alpha信号。我见过有人把因子对100个行业虚拟变量做回归,结果残差全是噪声,模型完全失效。

知识体系总览

下面这张图,是我自己梳理的数据预处理流程。每次做新项目,我都会先过一遍这个框架。

数据清洗与预处理流程 原始数据 缺失值处理 删除法 填充法 异常值检测 3σ法 IQR法 数据标准化 Z-score 排序标准化 中性化处理

整个流程走下来,数据才算真正「能用」。我见过太多人跳过这些步骤,直接跑模型,结果就是过拟合、回测失效。嗯,数据预处理虽然枯燥,但它是整个量化投资体系的基石。

总结一下:缺失值先看缺失率,高就删、低就补;异常值用IQR法更稳健;标准化选Z-score或排序法;中性化剔除市值和行业干扰。每一步都有坑,但踩过之后,你的模型会感谢你。


专注资料整理