4. 数据清洗与预处理:缺失值处理、异常值检测、数据重采样与对齐
原始金融数据在进入回测引擎之前,必须经过严格的清洗与预处理。缺失值、异常点、时间频率不一致等问题会直接导致标签错位、回测结果失真。本节将系统讲解数据清洗的核心步骤,确保输入到策略模型的数据干净、对齐、可复现。
4.1 缺失值处理
金融时间序列的缺失值通常由停牌、数据源中断、节假日差异等原因造成。处理策略需根据缺失比例与业务含义选择:
| 缺失比例 | 推荐方法 | 适用场景 |
|---|---|---|
| < 5% | 前向填充(ffill) | 价格、成交量等连续序列 |
| 5% – 20% | 线性插值 | 因子值、收益率序列 |
| > 20% | 删除该列或该时间段 | 流动性差、数据质量不可靠 |
| 特定业务缺失 | 标记为 NaN 并跳过 | 停牌日、非交易日 |
关键原则: 回测中禁止使用未来信息填充缺失值(如用未来收盘价填充当前缺失)。前向填充是最安全的默认选择。
import pandas as pd
import numpy as np
# 示例:日线价格缺失处理
df = pd.DataFrame({
'date': pd.date_range('2023-01-01', periods=10, freq='D'),
'close': [100, 101, np.nan, 103, 104, np.nan, np.nan, 107, 108, 109]
}).set_index('date')
# 前向填充(推荐)
df['close_ffill'] = df['close'].ffill()
# 线性插值(仅用于连续因子)
df['close_interp'] = df['close'].interpolate(method='linear')
print(df)
4.2 异常值检测
异常值可能来自数据录入错误、极端市场事件或数据源故障。常用的检测方法包括:
- Z-Score 方法: 假设数据服从正态分布,|Z| > 3 视为异常。
- IQR 方法: 超出 Q1 - 1.5×IQR 或 Q3 + 1.5×IQR 的数据点。
- MAD 方法: 基于中位数的稳健统计,适合金融尖峰厚尾分布。
实践建议: 对于收益率序列,优先使用 MAD 方法;对于价格序列,使用 IQR 方法并配合业务逻辑(如涨跌停限制)进行二次过滤。
from scipy import stats
def detect_outliers_mad(series, threshold=3.5):
"""基于中位数绝对偏差的异常检测"""
median = series.median()
mad = np.median(np.abs(series - median))
modified_z_scores = 0.6745 * (series - median) / mad
return np.abs(modified_z_scores) > threshold
# 示例:检测收益率异常
returns = pd.Series([0.01, -0.02, 0.03, 0.15, -0.01, 0.02, -0.03, 0.01, -0.10, 0.02])
outliers = detect_outliers_mad(returns)
print("异常值索引:", returns[outliers].index.tolist())
4.3 数据重采样
不同数据源的时间频率可能不一致(如日线、分钟线、Tick 级),回测前需要统一到目标频率。重采样规则需根据数据类型选择:
| 数据类型 | 降采样(高频→低频) | 升采样(低频→高频) |
|---|---|---|
| 价格(OHLC) | OHLC 聚合 | 前向填充 + 插值 |
| 成交量 | 求和 | 按比例分配或填充 0 |
| 收益率 | 复利合成 | 线性插值(谨慎使用) |
| 因子值 | 均值 / 最后值 | 前向填充 |
# 示例:将分钟线重采样为日线
df_minute = pd.DataFrame({
'close': [100, 101, 102, 103, 104, 105],
'volume': [1000, 1500, 1200, 1800, 900, 2000]
}, index=pd.date_range('2023-01-01 09:30', periods=6, freq='30min'))
# 降采样到日线
df_daily = df_minute.resample('D').agg({
'close': 'last',
'volume': 'sum'
})
print(df_daily)
4.4 时间对齐
多资产、多因子回测的核心难点在于时间对齐。不同股票的交易时间、停牌时段、节假日均不同,必须将数据对齐到统一的时间轴。
常见陷阱: 直接使用 pd.merge 按日期合并会导致未来信息泄露。例如,A 股 15:00 收盘,美股 16:00 收盘,若按自然日合并,会引入美股收盘后的信息。
对齐标准流程:
- 确定基准时间轴: 通常选择交易所共同交易日 + 统一频率(如日线)。
- 前向填充对齐: 每个资产/因子按基准时间轴进行前向填充,确保无未来数据。
- 截断处理: 删除基准时间轴中缺失比例超过阈值的日期。
- 验证对齐: 检查对齐后数据框的索引是否严格单调递增且无重复。
# 多资产对齐示例
dates = pd.date_range('2023-01-01', periods=5, freq='D')
asset_a = pd.Series([100, 101, np.nan, 103, 104], index=dates, name='A')
asset_b = pd.Series([200, np.nan, 202, 203, 204], index=dates, name='B')
# 创建统一时间轴
common_index = pd.date_range('2023-01-01', periods=5, freq='D')
# 对齐并前向填充
aligned = pd.DataFrame({
'A': asset_a.reindex(common_index).ffill(),
'B': asset_b.reindex(common_index).ffill()
})
# 删除仍包含 NaN 的行(可选)
aligned_clean = aligned.dropna()
print(aligned_clean)
4.5 清洗后的验证检查
数据预处理完成后,必须执行以下验证步骤,确保数据质量满足回测要求:
- 完整性检查: 确认无 NaN 值(允许的缺失标记除外)。
- 单调性检查: 时间索引严格递增,无重复时间戳。
- 范围检查: 价格、收益率、因子值在合理业务范围内。
- 对齐检查: 多资产/多因子在同一时间点无错位。
自动化检查脚本: 建议将上述检查封装为
validate_dataset() 函数,在每次回测启动前自动运行,输出检查报告。
def validate_dataset(df, name="数据集"):
"""数据清洗后验证"""
checks = {
"NaN 数量": df.isna().sum().sum(),
"重复索引": df.index.duplicated().sum(),
"索引单调性": df.index.is_monotonic_increasing,
"行数": len(df),
"列数": len(df.columns)
}
print(f"=== {name} 验证报告 ===")
for k, v in checks.items():
status = "✅" if (isinstance(v, bool) and v) or (isinstance(v, int) and v == 0) else "⚠️"
print(f"{status} {k}: {v}")
return all([
checks["NaN 数量"] == 0,
checks["重复索引"] == 0,
checks["索引单调性"]
])
# 使用示例
# is_valid = validate_dataset(aligned_clean, "对齐后数据")
经过上述清洗与预处理步骤,数据将具备进入回测引擎的基本条件。下一阶段将基于对齐后的数据构建因子与标签,确保回测的每一个信号都建立在干净、无偏的数据基础之上。