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使用,处理首尾 |
| 插值法 | 日内高频数据 | 线性插值,适合短时间缺失 |
| 直接删除 | 缺失比例极小 | 慎用,容易破坏时间序列连续性 |
4.2 处理异常值
异常值比缺失值更隐蔽。数据源偶尔会抽风,比如某天收盘价突然变成0,或者涨了100倍——这明显是错的。
我在项目中遇到过最离谱的一次,某只股票某天的成交量突然多了三个零,一看就是数据录入时多打了个0。如果不处理,计算出来的流动性指标直接爆炸。
常用的检测方法:
- 固定阈值法:比如涨跌幅超过±20%的,直接标记为异常
- 标准差法:超过均值±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)
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}")
知识体系总览
下面这张图,是我自己总结的数据清洗流程。每次拿到新数据,我都会按这个步骤走一遍:
嗯,数据清洗这部分,说白了就是「磨刀不误砍柴工」。我见过太多人急着写策略,结果数据没处理好,白白浪费了几个月的回测时间。你想想看,与其花时间调参数,不如先把数据搞干净——这才是量化回测最实在的捷径。