4、数据清洗与预处理:处理缺失值、处理异常值、数据对齐与重采样、复权数据处理

说实话,做量化回测这么多年,我踩过最大的坑,往往不是策略逻辑写错了,而是数据本身有问题。

你想想看,拿着有缺失、有异常、时间对不齐的数据去跑回测,结果再漂亮也是自欺欺人。我刚开始做的时候,就吃过这个亏——回测曲线漂亮得不行,实盘一跑直接崩了。后来一查,原来是某天的数据因为交易所故障漏了,而我压根没处理。

所以这一章,咱们就把数据清洗这件事聊透。说白了,就是让数据变得「干净、对齐、可用」。

4.1 处理缺失值

缺失值在金融数据里太常见了。停牌、节假日、数据源故障,都会导致某只股票某一天没有数据。

我个人习惯,拿到数据第一件事就是检查缺失情况:

import pandas as pd
import numpy as np

# 模拟一段有缺失的日线数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
price_data = pd.DataFrame({
    'close': [100, 101, np.nan, 103, 104, np.nan, 106, 107, 108, 109]
}, index=dates)

# 检查缺失值
print(price_data.isnull().sum())

处理方式主要有三种,我按推荐顺序列出来:

方法 适用场景 我的建议
向前填充(ffill) 停牌、非交易日 最常用,用上一个交易日数据填充
向后填充(bfill) 数据开头缺失 配合ffill使用,处理首尾
插值法 日内高频数据 线性插值,适合短时间缺失
直接删除 缺失比例极小 慎用,容易破坏时间序列连续性
我的小技巧: 对于股票数据,我一般先用ffill填充停牌日,再用bfill处理最前面几天的缺失。如果某只股票连续缺失超过5天,我会直接标记为「暂停交易」,而不是强行填充。

4.2 处理异常值

异常值比缺失值更隐蔽。数据源偶尔会抽风,比如某天收盘价突然变成0,或者涨了100倍——这明显是错的。

我在项目中遇到过最离谱的一次,某只股票某天的成交量突然多了三个零,一看就是数据录入时多打了个0。如果不处理,计算出来的流动性指标直接爆炸。

常用的检测方法:

  1. 固定阈值法:比如涨跌幅超过±20%的,直接标记为异常
  2. 标准差法:超过均值±3倍标准差的,视为异常
  3. 分位数法:低于1%分位数或高于99%分位数的,需要检查
# 用分位数法检测异常值
def detect_outliers(series, lower=0.01, upper=0.99):
    q_low = series.quantile(lower)
    q_high = series.quantile(upper)
    return (series < q_low) | (series > q_high)

# 举个例子
price_series = pd.Series([100, 101, 102, 9999, 103, 104, 0.5, 105])
outliers = detect_outliers(price_series)
print(f"异常位置: {outliers[outliers].index.tolist()}")
注意: 千万不要直接删除异常值!我曾经犯过这个错,删掉异常值后时间序列出现了断裂,导致后续计算收益率时全部错位。正确的做法是:先标记异常,然后用缺失值的方式处理(填充或插值)。

4.3 数据对齐与重采样

做多品种回测时,数据对齐是个大问题。A股和港股节假日不同,期货和股票交易时间不同,如果不做对齐,计算出来的组合收益全是错的。

我一般用 pd.merge 做外连接,然后统一填充:

# 模拟两只股票的数据,日期不完全一致
stock_a = pd.DataFrame({
    'date': ['2024-01-01', '2024-01-02', '2024-01-04'],
    'close_a': [100, 101, 103]
})
stock_b = pd.DataFrame({
    'date': ['2024-01-01', '2024-01-03', '2024-01-04'],
    'close_b': [200, 202, 205]
})

# 对齐到同一时间轴
merged = pd.merge(stock_a, stock_b, on='date', how='outer').sort_values('date')
merged = merged.fillna(method='ffill')  # 向前填充缺失值
print(merged)

重采样则是把不同频率的数据统一。比如把日线数据重采样成周线:

# 日线数据重采样为周线
daily_data = pd.DataFrame({
    'close': [100, 101, 102, 103, 104, 105, 106],
    'volume': [1000, 1100, 1200, 1300, 1400, 1500, 1600]
}, index=pd.date_range('2024-01-01', periods=7, freq='D'))

# 取每周最后一个交易日的收盘价,成交量求和
weekly_data = daily_data.resample('W').agg({
    'close': 'last',
    'volume': 'sum'
})
print(weekly_data)
核心原则: 数据对齐时,永远以「最稀疏」的时间轴为准。比如同时交易A股和港股,就以两者都开盘的日期作为有效交易日。

4.4 复权数据处理

复权处理是回测中最容易出问题的地方,没有之一。

为什么?因为股票会分红、送股、配股。如果不复权,价格会出现跳空,计算出来的收益率全是错的。

我见过太多人直接用不复权的数据跑回测,结果分红那天收益率突然暴跌,还以为策略出了问题。其实只是数据没处理对。

三种复权方式对比:

类型 原理 回测适用性
前复权 调整历史价格,使当前价格不变 适合看历史走势,但回测会引入未来信息
后复权 调整当前价格,使历史价格不变 适合计算真实收益率,但当前价格会失真
不复权 保持原始价格 需要手动处理分红除权,麻烦但最真实

我个人做回测时,习惯用后复权数据计算收益率,然后用前复权数据做可视化。这样既保证了收益计算的准确性,又让图表看起来正常。

# 模拟复权处理(简化版)
# 假设某股票在2024-01-03除权,每10股送1股
raw_prices = pd.Series([100, 101, 102, 90, 91, 92], 
                       index=pd.date_range('2024-01-01', periods=6, freq='D'))

# 后复权:除权日之后的价格乘以复权因子
# 复权因子 = 除权前价格 / 除权后理论价格
factor = 102 / 90  # 约1.1333
adjusted_prices = raw_prices.copy()
adjusted_prices['2024-01-03':] = raw_prices['2024-01-03':] * factor
print(f"后复权价格:\n{adjusted_prices}")
避坑指南: 我曾经直接用第三方库的复权数据,结果发现不同数据源的复权因子计算方式不一样。有的用流通股本加权,有的用总股本加权,算出来的结果能差好几个点。所以我现在都自己算复权因子,或者至少对比两个数据源。

知识体系总览

下面这张图,是我自己总结的数据清洗流程。每次拿到新数据,我都会按这个步骤走一遍:

数据清洗与预处理流程 原始金融数据 处理缺失值(ffill / bfill / 插值) 检测并处理异常值(阈值/标准差/分位数) 数据对齐与重采样(统一时间轴) 复权处理(前复权 / 后复权) 每一步都不可跳过,顺序执行才能得到干净可用的回测数据

嗯,数据清洗这部分,说白了就是「磨刀不误砍柴工」。我见过太多人急着写策略,结果数据没处理好,白白浪费了几个月的回测时间。你想想看,与其花时间调参数,不如先把数据搞干净——这才是量化回测最实在的捷径。

专注资料整理