数据清洗与预处理:量化分析的基石

做量化交易的朋友都知道一句话:垃圾进,垃圾出。数据质量不过关,再牛的策略也是白搭。今天我们就来聊聊数据清洗与预处理这个环节,说白了就是给原始数据「洗个澡」,让它变得干净、整齐、能用。

核心观点:数据预处理占整个量化项目60%以上的时间,但这是值得的。我见过太多人急着跑策略,结果被脏数据坑得血本无归。

数据清洗与预处理知识体系 原始金融数据 缺失值处理 异常值检测 复权价格计算 时间序列对齐 数据切片 重采样 干净、对齐、可用的数据

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

金融数据里缺失值太常见了。停牌、节假日、数据源故障,都会导致空值。我个人习惯先看看缺失比例,再决定怎么处理。

常用的处理方法:

  • 删除法:缺失比例小于5%时,直接删掉。简单粗暴,但有效。
  • 前向填充:用上一个非空值填充。适合价格数据,因为价格不会突变。
  • 后向填充:用下一个非空值填充。适合某些技术指标。
  • 插值法:线性插值、多项式插值。适合连续变化的数据。

我的经验:对于日频数据,我一般先用前向填充,再用后向填充补头尾。对于分钟级数据,线性插值效果更好。曾经有一次,我用前向填充处理了某只停牌股票的数据,结果回测收益虚高了一大截——因为停牌期间的价格没变,但实际复牌后直接跌停。嗯,从那以后我学会了先标记停牌日。

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, 106, 107, 108, np.nan]
})

# 前向填充
df['close_ffill'] = df['close'].ffill()

# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')

print(df)

二、异常值检测:揪出那些「离谱」的数据

异常值是什么?就是那些明显不符合规律的数据点。比如某只股票突然涨了1000%,或者成交量突然少了几个数量级。为什么会这样?可能是数据录入错误,也可能是除权除息没处理好。

常用的检测方法:

  1. 3σ原则:超出均值±3倍标准差的数据,视为异常。适合正态分布的数据。
  2. 箱线图法:用四分位数间距(IQR)来判定。上界=Q3+1.5×IQR,下界=Q1-1.5×IQR。
  3. 百分比截断:直接去掉前后1%或5%的数据。简单但粗暴。

注意:不要盲目删除异常值!有些异常值其实是真实的极端行情,比如2008年金融危机、2020年疫情暴跌。我曾经在回测中删除了这些「异常」数据,结果策略在实盘中遇到类似行情直接崩了。所以,先搞清楚异常的原因,再决定怎么处理。

# 箱线图法检测异常值
def detect_outliers_iqr(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    return outliers

# 使用示例
outliers = detect_outliers_iqr(df, 'close')
print(f"检测到 {len(outliers)} 个异常值")

三、复权价格计算:还原真实的交易场景

股票会分红、送股、配股,这些事件会导致价格出现「跳空」。如果不做复权处理,你的回测结果就是错的。说白了,复权就是消除这些事件的影响,让价格序列连续。

两种复权方式:

复权类型 计算方法 适用场景
前复权 调整历史价格,使当前价格不变 适合看历史走势,回测常用
后复权 调整当前价格,使历史价格不变 适合看真实收益,长期投资分析

我的建议:做回测时用前复权。为什么?因为前复权保证了当前价格是真实的,你的买卖价格不会失真。后复权虽然能反映真实收益,但当前价格被调整了,下单时会出问题。我记得有一次用后复权数据做回测,策略显示盈利50%,结果实盘一跑直接亏了——因为买入价对不上。

# 简单复权计算示例
def calc_adj_factor(dividend, close_before, close_after):
    """计算复权因子"""
    return 1 + dividend / close_after

# 假设某股票分红1元,除息日前后收盘价
dividend = 1.0
close_before = 101.0
close_after = 100.0

adj_factor = calc_adj_factor(dividend, close_before, close_after)
print(f"复权因子: {adj_factor:.4f}")
print(f"前复权调整后价格: {close_before * adj_factor:.2f}")

四、时间序列对齐:让不同数据「步调一致」

做多因子策略时,你需要同时用到价格、成交量、财务数据、宏观经济数据。这些数据的时间频率不一样,有的日频,有的周频,有的季度频。怎么把它们对齐到同一个时间轴上?

我个人习惯用「左对齐」或「右对齐」的方式:

  • 左对齐:用上一个已知值填充当前时间点。适合财务数据,因为财报发布有滞后。
  • 右对齐:用下一个已知值填充。适合某些前瞻性指标。

避坑指南:我曾经犯过一个错误——把季度财报数据直接对齐到每个交易日,然后用当天的财报数据做交易决策。但财报是季度末才发布的,你根本不可能在季度中间就知道这些数据。所以,对齐时一定要考虑「信息发布时间」,而不是「数据所属时间」。

# 时间序列对齐示例
price_data = pd.DataFrame({
    'date': pd.date_range('2023-01-01', periods=5, freq='D'),
    'price': [100, 101, 102, 103, 104]
})

# 财务数据(季度频率)
financial_data = pd.DataFrame({
    'date': pd.date_range('2023-01-01', periods=2, freq='Q'),
    'eps': [1.2, 1.5]
})

# 左对齐:用上一个季度的EPS填充
price_data['date'] = pd.to_datetime(price_data['date'])
financial_data['date'] = pd.to_datetime(financial_data['date'])

aligned = pd.merge_asof(
    price_data.sort_values('date'),
    financial_data.sort_values('date'),
    on='date',
    direction='backward'  # 左对齐
)
print(aligned)

五、数据切片与重采样:灵活处理不同频率的数据

数据切片,说白了就是按时间范围截取数据。比如我只想看2023年1月到3月的数据。重采样呢,就是把数据从一种频率转换成另一种频率,比如把分钟数据变成日数据。

重采样的常见操作:

  • 降采样:高频转低频。比如分钟转日,用收盘价作为日价格,成交量求和。
  • 升采样:低频转高频。比如日转分钟,需要插值或填充。

注意:降采样时,OHLC(开盘、最高、最低、收盘)的处理方式不同。开盘价取第一个,最高价取最大值,最低价取最小值,收盘价取最后一个。成交量要累加。千万别搞混了!

# 重采样示例
# 假设有分钟级数据
minute_data = pd.DataFrame({
    'date': pd.date_range('2023-01-01 09:30', periods=240, freq='1min'),
    'close': np.random.randn(240).cumsum() + 100,
    'volume': np.random.randint(1000, 5000, 240)
})

# 降采样为日数据
daily_data = minute_data.resample('D', on='date').agg({
    'close': 'last',
    'volume': 'sum'
})

print(daily_data.head())

总结一下

数据清洗与预处理,说白了就是给量化分析打好地基。缺失值处理、异常值检测、复权价格计算、时间序列对齐、数据切片与重采样,这五个步骤缺一不可。我做了这么多年量化,最大的体会就是:数据准备花的时间越多,后面跑策略就越省心。

记住一句话:好的数据是成功策略的一半。别急着跑模型,先把数据搞干净再说。


专注资料整理