数据预处理:金融时间序列的清洗、缺失值与异常值处理
做量化交易,有一句话我特别认同:垃圾进,垃圾出。你模型再牛,策略再花哨,数据一塌糊涂,结果就是白忙活。我自己早期做CTA策略时,就因为没处理好一个跳空缺口,回测曲线漂亮得不行,实盘直接亏到怀疑人生。嗯,从那以后,数据预处理这块我再也不敢马虎了。
今天咱们就聊聊金融时间序列的预处理。说白了,就是把原始数据收拾干净,让它能喂给模型吃。
一、金融数据的“脏”从哪来?
你想想看,我们拿到的行情数据,其实是从交易所、数据商那里一层层传过来的。中间任何一个环节出问题,数据就变味了。常见的“脏数据”有这几类:
- 缺失值:某一天K线没数据,或者某分钟的交易量为0
- 异常值:价格突然跳到一个离谱的位置,比如某股票瞬间涨了1000%
- 重复值:同一时间戳出现两条相同的数据
- 时间戳错乱:数据的时间顺序不对,或者时间间隔不均匀
我个人习惯,拿到数据第一件事就是画个时间序列图。肉眼扫一遍,基本能看出问题。别小看这个土办法,很多坑都是这么发现的。
二、缺失值处理:不能直接删,也不能瞎填
金融数据里的缺失值,处理起来要格外小心。为什么?因为金融数据有很强的自相关性和趋势性。你随便填一个均值进去,可能会破坏原有的价格走势结构。
我常用的几种方法:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 向前填充(ffill) | 非交易时段、停牌 | 连续缺失太多时慎用 |
| 向后填充(bfill) | 数据开头缺失 | 会引入未来信息 |
| 线性插值 | 日内高频数据 | 适合短时间间隔 |
| 模型预测填充 | 关键特征缺失 | 计算量大,慎用 |
举个例子,处理日线数据的缺失:
import pandas as pd
import numpy as np
# 假设df是包含日期索引的DataFrame
# 向前填充:用上一个交易日的数据填充
df['close'].fillna(method='ffill', inplace=True)
# 如果连续缺失超过3天,我建议标记出来
df['missing_flag'] = df['close'].isna().astype(int)
df['missing_group'] = df['missing_flag'].ne(df['missing_flag'].shift()).cumsum()
df['missing_count'] = df.groupby('missing_group')['missing_flag'].transform('sum')
# 连续缺失超过5天的,直接删除
df = df[~((df['missing_flag'] == 1) & (df['missing_count'] > 5))]
我的经验:处理缺失值时,一定要记录缺失的分布。如果某只股票经常连续几天没数据,那可能是流动性太差,这种标的我一般直接排除。
三、异常值检测:别把“黑天鹅”当噪声
异常值检测是个技术活。金融数据里的异常,有些是数据错误,有些是真实的市场异动。你想想看,如果某天突然出现一个-30%的跌幅,到底是数据错了,还是真的发生了熔断?
我常用的检测方法:
- 标准差法(Z-score):假设数据服从正态分布,超过3个标准差的视为异常
- 四分位距法(IQR):用Q1-1.5*IQR和Q3+1.5*IQR作为边界
- 百分比法:直接设定涨跌幅阈值,比如超过±20%的标记出来
- 滚动窗口法:用过去N天的均值和标准差动态判断
我个人最推荐的是滚动窗口法。为什么?因为金融数据的分布是时变的。牛市里波动大,熊市里波动小,用固定阈值很容易误判。
def detect_outliers_rolling(df, column='close', window=20, n_std=3):
"""
滚动窗口异常值检测
"""
df = df.copy()
df['rolling_mean'] = df[column].rolling(window=window).mean()
df['rolling_std'] = df[column].rolling(window=window).std()
# 计算Z-score
df['z_score'] = (df[column] - df['rolling_mean']) / df['rolling_std']
# 标记异常
df['is_outlier'] = np.abs(df['z_score']) > n_std
return df
# 使用示例
df = detect_outliers_rolling(df, column='close', window=20, n_std=3)
outliers = df[df['is_outlier'] == True]
print(f"检测到 {len(outliers)} 个异常点")
注意:我曾经在回测中直接删除了所有异常值,结果回测曲线漂亮得不像话。后来才发现,那些“异常值”其实是真实的熔断事件。删除它们等于删除了重要的尾部风险信息。所以,异常值要标记,不要轻易删除。
四、处理策略:分类讨论
检测出异常值后,怎么处理?我一般分三种情况:
- 明显的数据错误(比如价格为负数、交易量为0但价格有变动):直接删除或修正
- 可能的真实异动(比如涨跌停、熔断):保留,但打上标签
- 可疑的跳空(比如隔夜跳空过大):用前收盘价+合理波动范围进行修正
举个例子,处理涨跌停数据:
# 假设A股,涨跌幅限制为±10%
df['pct_change'] = df['close'].pct_change()
df['is_limit_up'] = df['pct_change'] >= 0.095
df['is_limit_down'] = df['pct_change'] <= -0.095
# 对于涨停/跌停的数据,不修改价格,但标记出来
# 计算指标时,可以单独处理这些标记
五、知识体系总览
说了这么多,咱们用一张图把整个流程串起来:
这张图把整个流程串起来了。从原始数据开始,先做清洗,再处理缺失值,然后检测异常值,最后分类处理。每一步都有对应的技术手段,不能跳步。
六、避坑指南
最后,分享几个我踩过的坑:
- 不要一次性处理所有数据:不同时间尺度、不同品种的数据,处理方式可能完全不同。日线和分钟线,股票和期货,处理逻辑都不一样。
- 保留原始数据:我习惯在预处理前先备份一份原始数据。万一处理错了,还能回滚。
- 记录处理日志:每次处理,都记录下删除了多少数据、填充了多少缺失值。这样回测出问题时,能快速定位。
- 注意未来信息:用向后填充或者全局统计量时,很容易引入未来信息。做回测时一定要小心,别让模型“偷看”了未来的数据。
核心原则:数据预处理的目标不是让数据“好看”,而是让数据“真实”。保留市场本来的面貌,哪怕它很丑陋。