第四章:数据清洗与预处理——别让脏数据毁了你的策略

做量化投研这几年,我最大的感悟就是:数据决定上限,模型只是逼近这个上限。你想想看,再牛的Transformer,喂进去一堆垃圾,吐出来的还是垃圾。这一章,咱们就聊聊数据清洗那些绕不开的坑。

核心观点:数据预处理占整个投研流程的60%-80%时间,但决定了策略的生死。

4.1 缺失值处理——别急着删,先问问为什么

拿到数据第一件事,我习惯先跑个 df.isnull().sum()。但注意,缺失不是原罪,要看缺失的机制

我在项目中遇到过一种情况:某只小盘股突然连续3天没有交易数据。一开始我以为是数据源问题,后来一查,原来是停牌了。这种缺失你要是直接删掉或者填充,策略逻辑就全乱了。

常见的缺失值处理策略

缺失类型 处理方法 适用场景
完全随机缺失 删除或均值填充 数据量大,缺失比例<5%
随机缺失 插值法、KNN填充 时间序列数据
非随机缺失 单独建模处理 停牌、退市等特殊事件

我的习惯:金融数据优先用前向填充(ffill),因为市场信息是逐步释放的。用未来数据填充过去,那是作弊。

# 我个人最常用的缺失值处理流水线
import pandas as pd
import numpy as np

def clean_missing(df, method='ffill', limit=3):
    """
    method: ffill / bfill / interpolate / drop
    limit: 连续缺失最大容忍天数
    """
    if method == 'ffill':
        return df.fillna(method='ffill', limit=limit)
    elif method == 'interpolate':
        return df.interpolate(method='time', limit_direction='forward')
    elif method == 'drop':
        return df.dropna(thresh=len(df.columns) * 0.7)  # 保留70%以上非空的行

4.2 异常值检测——那些让你亏钱的数据点

异常值在量化里分两种:一种是数据错误(比如价格突然跳了100倍),另一种是真实极端事件(比如2015年股灾)。

我曾经犯过一个错:用3σ原则把2015年6月的所有数据都标为异常然后删掉了。结果回测曲线漂亮得不行,实盘直接被打脸。为什么?因为我把真正的尾部风险给洗掉了。

避坑指南:异常值检测要分两步走——先识别技术异常(数据录入错误),再标记业务异常(极端行情)。前者要处理,后者要保留并单独分析。

常用的异常检测方法

  • Z-Score法:适合正态分布数据,阈值一般取3
  • IQR四分位法:适合偏态分布,用Q1-1.5IQR和Q3+1.5IQR
  • 孤立森林:适合高维数据,我最近在因子分析里常用
def detect_outliers_iqr(series, factor=1.5):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - factor * IQR
    upper = Q3 + factor * IQR
    return (series < lower) | (series > upper)

# 注意:金融数据用1.5倍太激进,我一般用3倍

4.3 数据类型转换——隐形的性能杀手

嗯,这里要注意。很多新手拿到数据直接开搞,结果跑起来慢得想砸电脑。我检查过他们的代码,发现日期列是字符串,价格列是object类型

数据类型不对,不仅慢,还会算错。比如 '2024-01-01'2024-01-01 在排序时结果完全不同。

原始类型 目标类型 转换方法
字符串日期 datetime64 pd.to_datetime()
object数值 float64 pd.to_numeric(errors='coerce')
int64 float32 节省50%内存

小技巧:处理百万级数据时,把int64转成int32或float32,内存占用直接砍半。我有个策略原来跑要8G内存,优化后2G就够了。

4.4 时间序列对齐——不同频率数据的缝合术

做多因子模型时,你经常会遇到:日线数据、分钟数据、财报数据(季度)、宏观数据(月度)。这些数据频率不同,怎么对齐?

说白了,就是以最细粒度的时间戳为基准,把其他数据填充过来。我习惯用 pd.merge_asof,它比普通的merge更智能,能自动找到最近的时间点。

# 日线数据和财报数据对齐
daily_prices = pd.DataFrame({'date': pd.date_range('2024-01-01', periods=100, freq='D')})
quarterly_earnings = pd.DataFrame({
    'date': pd.to_datetime(['2024-01-15', '2024-04-15', '2024-07-15']),
    'eps': [1.2, 1.5, 1.8]
})

# 用asof对齐,每个交易日取最近一次财报数据
aligned = pd.merge_asof(daily_prices, quarterly_earnings, on='date', direction='backward')

注意:对齐时一定要考虑「未来信息泄露」。比如用季度数据时,财报发布日期和财报截止日期是两回事。我见过有人直接用截止日期对齐,结果用未来数据预测过去,回测曲线漂亮得离谱。

4.5 数据标准化与归一化——让不同量纲的因子公平竞争

你想想看,市盈率可能是5-50,换手率是0.1%-5%,市值是10亿-10000亿。如果不做标准化,模型会天然偏向数值大的特征。

我个人习惯分两步:先去极值,再标准化。去极值用MAD(中位数绝对偏差),比Z-Score更稳健。

方法 公式 输出范围 适用场景
Z-Score标准化 (x - μ) / σ 无固定范围 数据近似正态分布
Min-Max归一化 (x - min) / (max - min) [0, 1] 有明确上下界
Rank归一化 排序后映射到[0,1] [0, 1] 因子有异常值
def robust_standardize(series):
    """稳健标准化:用中位数和MAD代替均值和标准差"""
    median = series.median()
    mad = (series - median).abs().median()
    # 防止除以0
    mad = max(mad, 1e-8)
    return (series - median) / (1.4826 * mad)  # 1.4826使MAD与标准差一致

我的经验:做截面标准化时(比如每天对所有股票的因子做标准化),一定要按日期分组。如果你对整个时间序列做标准化,相当于用了未来信息,回测会严重高估。

知识体系总览

下面这张图是我自己梳理的数据预处理流程,每次做新策略前都会过一遍:

数据清洗与预处理 · 知识体系 原始数据 缺失值处理 ffill填充 · bfill填充 · 插值法 · 删除策略 停牌处理 · 财报缺失 · 交易日对齐 异常值检测 Z-Score · IQR四分位 · 孤立森林 · MAD方法 区分技术异常与业务异常 · 尾部风险保留 数据类型转换 datetime64 · float32/64 · category类型 内存优化 · 性能提升 · 避免隐式类型错误 时间序列对齐 merge_asof · 重采样 · 不同频率数据缝合 避免未来信息泄露 · 财报日期 vs 截止日期 标准化与归一化 → 干净数据 按顺序执行,不可跳过

这张图我每次做新策略都会贴在墙上。你想想看,如果数据这关没过,后面所有模型工作都是白费。我见过太多团队花3个月调模型,最后发现是数据对齐出了问题。

总结一句话:数据清洗不是体力活,是策略的第一道风控。把这一步做扎实了,后面的建模工作会顺畅很多。

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