4、数据清洗:处理缺失值、异常值、时间戳对齐与重采样

数据清洗这事儿,说白了就是给策略喂饭前先挑挑沙子。我见过太多人,策略逻辑写得漂漂亮亮,结果一跑回测全是坑——为什么?数据没洗干净。今天咱们就把这块硬骨头啃下来。

4.1 缺失值处理:别让空值毁了你的策略

做套利最怕什么?两个品种的数据对不上。一个品种有报价,另一个缺了,你的价差瞬间变成NaN。我个人习惯,拿到数据第一件事就是检查缺失率。

核心原则:缺失率低于5%可以填充,高于20%建议重新获取数据源。

4.1.1 常见填充方法

  • 向前填充(ffill):用上一个有效值填充。适合高频数据,比如1分钟K线,价格变化不大。
  • 向后填充(bfill):用下一个有效值填充。适合收盘后补数据。
  • 线性插值:用前后两个值的均值。适合缓慢变化的数据,比如指数成分股调整。
  • 删除缺失行:如果缺失比例极小,直接删掉最省事。
import pandas as pd
import numpy as np

# 模拟数据
df = pd.DataFrame({
    'time': pd.date_range('2024-01-01', periods=10, freq='1min'),
    'price': [100, 101, np.nan, 103, 104, np.nan, 106, 107, 108, np.nan]
})

# 向前填充
df['price_ffill'] = df['price'].ffill()

# 线性插值
df['price_interp'] = df['price'].interpolate()

print(df)

我的经验:做期货套利时,我习惯用ffill。因为期货价格连续,跳空概率低。但做股票就不一样了——停牌导致的数据缺失,千万别填充,直接删掉那行。我曾经吃过这个亏,填充了停牌数据,结果策略在停牌期间疯狂开仓,回测曲线漂亮得不像话,实盘直接崩了。

4.2 异常值检测:把那些离谱的数据揪出来

异常值是什么?就是那些明显不符合市场规律的数据。比如某只股票突然涨了1000%,或者价格变成负数。嗯,这里要注意,异常值不一定是错误,也可能是真实事件(比如乌龙指)。

4.2.1 常用检测方法

方法 适用场景 阈值
Z-score 正态分布的数据 |Z| > 3
IQR(四分位距) 偏态分布的数据 Q1 - 1.5*IQR 或 Q3 + 1.5*IQR
百分比截断 极端值明显的场景 上下1%或5%
# 使用IQR检测异常值
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]
print(f"发现 {len(outliers)} 个异常值")

避坑指南:我曾经在回测国债期货套利时,用Z-score检测异常值,结果把2016年11月的“债灾”数据全删了。那波行情恰恰是策略最该学习的极端情况。所以,异常值要分情况处理:如果是数据录入错误,直接删;如果是真实市场事件,保留并标记。

4.3 时间戳对齐:让两个品种的数据步调一致

做套利策略,最核心的就是时间对齐。你想想看,如果A品种的报价是10:00:01,B品种是10:00:03,这两笔数据能直接算价差吗?当然不能。时间戳不对齐,价差全是错的。

4.3.1 对齐策略

  • 精确对齐:找到时间戳完全一致的数据。适合高频数据,但数据量会减少。
  • 最近邻对齐:用最接近的时间戳匹配。适合不同频率的数据。
  • 向前对齐:用上一个时间戳的数据匹配当前时间戳。适合处理延迟数据。
# 假设有两个品种的数据
df_a = pd.DataFrame({
    'time': pd.to_datetime(['2024-01-01 10:00:01', '2024-01-01 10:00:03', '2024-01-01 10:00:05']),
    'price_a': [100, 101, 102]
})

df_b = pd.DataFrame({
    'time': pd.to_datetime(['2024-01-01 10:00:02', '2024-01-01 10:00:04', '2024-01-01 10:00:06']),
    'price_b': [200, 201, 202]
})

# 使用merge_asof进行最近邻对齐
df_aligned = pd.merge_asof(df_a, df_b, on='time', direction='nearest')
print(df_aligned)

我的习惯:做跨期套利时,我一般用向前对齐。因为主力合约切换时,近月合约的流动性更好,报价更新更快。用近月合约的时间戳去对齐远月合约,能减少很多伪信号。

4.4 重采样:统一数据频率

数据频率不统一,是套利回测的常见问题。一个品种是1分钟数据,另一个是5分钟数据,怎么算?重采样就是干这个的。

4.4.1 重采样方法

频率转换 方法 示例
降采样(高频→低频) 取均值、OHLC 1分钟→5分钟
升采样(低频→高频) 插值、填充 日线→小时线
# 将1分钟数据重采样为5分钟
df_resampled = df.set_index('time').resample('5min').agg({
    'price': 'ohlc',  # 生成开盘、最高、最低、收盘
    'volume': 'sum'
})

print(df_resampled.head())

关键点:降采样时,价格用OHLC(开盘价、最高价、最低价、收盘价),成交量用求和。升采样时,价格用插值,成交量用0填充(因为低频到高频,成交量无法拆分)。

4.5 本章知识体系

下面这张图,把数据清洗的四个核心步骤串起来了。你可以把它当作操作手册,每次做回测前对照检查一遍。

数据清洗流程 原始数据 步骤1:缺失值处理 ffill / bfill / 插值 / 删除 步骤2:异常值检测 Z-score / IQR / 百分比截断 步骤3:时间戳对齐 精确对齐 / 最近邻 / 向前对齐 步骤4:重采样 降采样(OHLC) / 升采样(插值) 清洗后的数据 → 回测

数据清洗这件事,看起来琐碎,但直接影响策略的生死。我见过太多人,策略逻辑写得天花乱坠,结果数据没洗干净,回测曲线漂亮得不像真的。记住一句话:垃圾进,垃圾出。数据干净了,策略才有意义。