4. 数据清洗与预处理
做量化的人都知道一句话:垃圾进,垃圾出。你模型再牛,策略再精巧,数据一塌糊涂,结果就是白忙活。我刚开始做K‑quant那会,就吃过这亏——回测曲线漂亮得像造假,一上实盘立马翻车。查了半天,原来是有个交易所的行情数据缺了半小时,我的策略在那边狂开仓,实际根本没法成交。嗯,从那以后,数据清洗这块我再也不敢糊弄了。
这一章咱们就聚焦三个最常踩坑的环节:缺失值填充、时间戳对齐、多周期重采样。说白了,就是把你手头乱七八糟的原始数据,收拾成能直接喂给量化模型的干净数据。
4.1 缺失值处理——别让空数据拖后腿
金融数据里缺失值太常见了。交易所断流、行情快照没抓到、节假日停盘……原因五花八门。但模型不认识「缺失」,它只会报错或者偷偷给你算出荒谬的结果。
我个人的习惯是:拿到数据第一件事,先看缺失率。 用 df.isnull().sum() 扫一眼,心里就有数了。缺失率超过 50% 的列,我通常直接扔掉——硬填回去也是噪声。
常用的填充方法有这么几种:
- 前向填充(ffill):用上一个非空值往下填。做高频数据时我最爱用这个。想想看,行情断了一秒,用上一秒的价格有什么问题?没大毛病。
- 后向填充(bfill):跟上面相反,用下一个非空值往上填。用得少,但特殊场景有用——比如你知道某个数据点是结算价,后面的缺失可以用它补。
- 线性插值:在两个有效值之间拉一条直线,填补中间的点。适合价格序列,但不适合跳跃性大的数据(比如成交量)。
- 统计量填充:用均值、中位数填充。我一般只在非时序数据里用,时序里用这个容易破坏趋势。
实战建议: 做因子计算时,缺失值填充顺序很有讲究。我个人先尝试前向填充,如果还有空位再用线性插值兜底。两层下来,基本上能填干净 99% 的数据。
import pandas as pd
# 前向填充,限制最大填充间距为 5 行
df['close'].ffill(inplace=True, limit=5)
# 剩余缺失用线性插值
df['close'].interpolate(method='linear', inplace=True)
# 实在填不了的,直接删除
df.dropna(subset=['close'], inplace=True)
避坑: 我曾经在批量回测时用了全局均值填充,结果把不同股票的收益率全拉到同一个水平线,策略筛选出来全是无效信号……查了三天才找到原因。从那以后,我坚持「按截面分组填充」,每只股票独立操作。
4.2 时间戳对齐——把不同频道的钟调准
做过多数据源的同学都有体会:A 交易所的时间戳精确到毫秒,B 交易所只到秒,C 数据商给你的 CSV 里时间还是字符串……要把它们拧到一起,靠的就是对齐。
时间戳对齐说白了就三件事:统一格式、统一时区、统一刻度。
我最常用的是 pd.merge_asof,这东西简直是多源时间序列的救星。它可以按「最接近的时间」把两个 DataFrame 拼起来,比普通的 merge 灵活得多。
# 示例:把 tick 数据和 1 秒快照对齐
tick_data = pd.DataFrame({
'time': pd.to_datetime(['2024-01-05 09:30:00.123',
'2024-01-05 09:30:00.456']),
'price': [101.2, 101.5]
})
snap_data = pd.DataFrame({
'time': pd.to_datetime(['2024-01-05 09:30:00.000',
'2024-01-05 09:30:01.000']),
'bid': [101.0, 101.3],
'ask': [101.4, 101.7]
})
# 按时间最近对齐,方向为 'nearest'
aligned = pd.merge_asof(tick_data, snap_data,
on='time', direction='nearest')
print(aligned)
direction 参数有三个选项:nearest(找最近的时间点)、forward(往后找最近)、backward(往前找最近)。怎么选?
- 回测场景:用
backward或nearest,避免用到未来数据。 - 实时场景:用
forward,拿最新的快照匹配当前 tick。
小技巧: 对齐前务必把时间戳设成 index 并排序。我见过太多人 merge_asof 报错,就是忘了 .sort_values('time')。嗯,这个坑我自己就踩过两回。
4.3 多周期数据重采样——统一你的时间刻度
量化里不同策略喜欢不同频率的数据。日内策略用 1 分钟线,CTA 策略用 1 小时线,组合管理可能只看日线。但你拿到的原始数据往往是 tick 级或者固定频率的,这就需要重采样。
重采样分两种:
- 降采样:从高频到低频(比如 1 分钟 → 5 分钟)。关键是选对聚合函数。
- 升采样:从低频到高频(比如 日线 → 1 小时)。需要插值或填充。
我把自己常用的几种模式列一下:
| 目标周期 | 原始频率 | 聚合方式 | 代码示例 |
|---|---|---|---|
| 5 分钟 K 线 | 1 分钟 | OHLC + 成交量求和 | df.resample('5min').ohlc() |
| 1 小时均值 | 5 分钟 | 均值(或中位数) | df.resample('1H').mean() |
| 日线波动率 | 1 小时 | 标准差 | df.resample('1D').std() |
| 周线持仓量 | 日线 | 最后值 | df.resample('W').last() |
这里有个关键点:升采样时要小心引入未来信息。 比如你把日线升采样到小时线,用线性插值填充中间的小时数据,这在回测里等于提前知道了收盘价。我一般只用前向填充或者不做升采样,宁可数据稀疏一点,也别作弊。
# 降采样:1min → 5min,生成标准 OHLC
df_5min = df_1min.resample('5min').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
# 升采样:日线 → 1小时,仅用前向填充(安全)
df_hourly = df_daily.resample('1H').ffill(limit=4)
注意: 重采样后的数据量会变少(降采样)或变多(升采样)。我吃过亏的是:降采样后忘记 dropna,导致一堆 NaN 行混进模型训练集,loss 直接飞了。建议每次重采样后都检查一下索引是否连续、值是否完整。
4.4 三步骤的配合顺序
这三个步骤不是独立的,它们有严格的先后关系:
- 先处理缺失值——不然对齐和重采样都会把缺失放大。
- 再做时间戳对齐——让多源数据站在同一时间点上。
- 最后重采样——统一到目标频率,供给策略使用。
顺序错了会怎样?我举个真实例子:有一次我先重采样再填充缺失,结果重采样把一段缺失区域「凝聚」成了一个错误的 K 线,开盘价比实际高了一大截,回测信号全部提前……嗯,那周我加了好几天班才把问题定位到。
我的标准 pipeline:
原始数据 → 按股票分组 → ffill(limit=5) → interpolate(linear) → dropna → 统一时间戳(merge_asof) → resample(target_freq) → 输出清洗后数据
这套流程我用了两年,在几十个策略上验证过,基本没再出过数据层面的幺蛾子。