数据预处理:金融时间序列的清洗、缺失值与异常值处理

做量化交易,有一句话我特别认同:垃圾进,垃圾出。你模型再牛,策略再花哨,数据一塌糊涂,结果就是白忙活。我自己早期做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%的跌幅,到底是数据错了,还是真的发生了熔断?

我常用的检测方法:

  1. 标准差法(Z-score):假设数据服从正态分布,超过3个标准差的视为异常
  2. 四分位距法(IQR):用Q1-1.5*IQR和Q3+1.5*IQR作为边界
  3. 百分比法:直接设定涨跌幅阈值,比如超过±20%的标记出来
  4. 滚动窗口法:用过去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

# 对于涨停/跌停的数据,不修改价格,但标记出来
# 计算指标时,可以单独处理这些标记

五、知识体系总览

说了这么多,咱们用一张图把整个流程串起来:

金融时间序列数据预处理流程 原始行情数据 第一步:数据清洗 检查时间戳 · 去重 · 排序 · 对齐 第二步:缺失值处理 ffill · bfill · 插值 · 模型预测 第三步:异常值检测 Z-score · IQR · 滚动窗口 · 阈值法 第四步:处理与标记 修正 · 删除 · 标记保留 · 分类处理

这张图把整个流程串起来了。从原始数据开始,先做清洗,再处理缺失值,然后检测异常值,最后分类处理。每一步都有对应的技术手段,不能跳步。

六、避坑指南

最后,分享几个我踩过的坑:

  • 不要一次性处理所有数据:不同时间尺度、不同品种的数据,处理方式可能完全不同。日线和分钟线,股票和期货,处理逻辑都不一样。
  • 保留原始数据:我习惯在预处理前先备份一份原始数据。万一处理错了,还能回滚。
  • 记录处理日志:每次处理,都记录下删除了多少数据、填充了多少缺失值。这样回测出问题时,能快速定位。
  • 注意未来信息:用向后填充或者全局统计量时,很容易引入未来信息。做回测时一定要小心,别让模型“偷看”了未来的数据。

核心原则:数据预处理的目标不是让数据“好看”,而是让数据“真实”。保留市场本来的面貌,哪怕它很丑陋。


蓝海数据掘金营,专注资料整理