数据清洗与预处理:量化分析的基石
做量化交易的朋友都知道一句话:垃圾进,垃圾出。数据质量不过关,再牛的策略也是白搭。今天我们就来聊聊数据清洗与预处理这个环节,说白了就是给原始数据「洗个澡」,让它变得干净、整齐、能用。
核心观点:数据预处理占整个量化项目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%,或者成交量突然少了几个数量级。为什么会这样?可能是数据录入错误,也可能是除权除息没处理好。
常用的检测方法:
- 3σ原则:超出均值±3倍标准差的数据,视为异常。适合正态分布的数据。
- 箱线图法:用四分位数间距(IQR)来判定。上界=Q3+1.5×IQR,下界=Q1-1.5×IQR。
- 百分比截断:直接去掉前后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())
总结一下
数据清洗与预处理,说白了就是给量化分析打好地基。缺失值处理、异常值检测、复权价格计算、时间序列对齐、数据切片与重采样,这五个步骤缺一不可。我做了这么多年量化,最大的体会就是:数据准备花的时间越多,后面跑策略就越省心。
记住一句话:好的数据是成功策略的一半。别急着跑模型,先把数据搞干净再说。