数据清洗与预处理:处理缺失值、异常值、复权价格计算、收益率序列生成
做量化回测,最怕什么?
不是策略亏钱,而是数据本身有问题。我见过太多人,策略写得漂漂亮亮,回测曲线亮瞎眼,结果一上实盘就崩。为什么?数据没洗干净。
这一章,咱们就聊聊数据清洗与预处理。说白了,就是让原始数据变得「能用」。我个人习惯把这一步叫做「数据炼油」——原油不能直接烧,得提炼。数据也一样。
核心要点:数据清洗占整个量化项目60%以上的时间。别嫌烦,这一步省了,后面全是坑。
一、处理缺失值:别让空值毁了你的回测
缺失值,就是数据表里那些空着的格子。股票停牌、节假日、数据源抽风,都会产生缺失值。
我刚开始做回测时,直接用了dropna()把缺失行全删了。结果呢?回测曲线漂亮得不行,但实盘时发现,那些被删掉的停牌日,恰恰是策略该调仓的日子。嗯,这里要注意——删除不是万能的。
常见的处理方法
- 向前填充(ffill):用上一个有效值填充。适合价格数据,因为停牌后复牌,价格通常延续。
- 向后填充(bfill):用下一个有效值填充。适合某些财务指标。
- 插值法:线性插值、多项式插值。适合中间缺失较少的情况。
- 直接删除:缺失比例极低时可用,但慎用。
import pandas as pd
import numpy as np
# 模拟数据
dates = pd.date_range('2024-01-01', periods=10, freq='D')
prices = [100, 101, np.nan, np.nan, 104, 105, np.nan, 107, 108, 109]
df = pd.DataFrame({'price': prices}, index=dates)
# 向前填充(我个人最常用)
df['price_ffill'] = df['price'].ffill()
# 线性插值
df['price_interp'] = df['price'].interpolate(method='linear')
print(df)
我的经验:处理股票日线数据时,我习惯用ffill()。为什么?因为停牌期间,你的资金其实是被锁定的,用前收盘价填充最符合实际情况。我曾经试过插值法,结果回测里出现了「不存在的价格」,实盘根本买不到。
二、异常值处理:揪出那些「离谱」的数据
异常值,就是那些明显不合理的数据点。比如某股票突然一天涨了1000%,或者成交量突然少了几个零。你想想看,这正常吗?
为什么会这样?数据录入错误、除权除息没处理、数据源bug,都可能导致异常值。
检测异常值的常用方法
- 3σ原则:超出均值±3倍标准差的数据,视为异常。
- IQR方法:低于Q1-1.5*IQR或高于Q3+1.5*IQR的数据,视为异常。
- 业务规则:比如涨跌幅超过20%的,直接标记。
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
# 假设df是某只股票的日线数据
outliers = detect_outliers_iqr(df, 'price')
print(f"发现 {len(outliers)} 个异常值")
避坑指南:我曾经用3σ原则处理过一只长期横盘的股票,结果把正常的波动都当成了异常值。后来我学乖了——异常值检测一定要结合业务场景。比如A股有涨跌停板限制,超过10%的日涨跌幅基本就是数据错误。
三、复权价格计算:还原真实的交易场景
复权,是量化回测里最容易踩的坑之一。
股票会分红、送股、配股。这些事件发生后,价格会跳空。如果你直接用原始价格做回测,你会发现策略在除权日莫名其妙亏了一笔钱——其实不是亏了,是价格调整了。
我个人习惯用后复权价格。为什么?因为后复权能真实反映持有期间的累计收益。
前复权 vs 后复权
| 类型 | 计算方法 | 适用场景 |
|---|---|---|
| 前复权 | 调整历史价格,使当前价格不变 | 技术分析、看K线形态 |
| 后复权 | 调整当前价格,使历史价格不变 | 计算真实收益率 |
def calc_adj_factor(dividend, split_ratio):
"""
计算复权因子
dividend: 每股分红
split_ratio: 送转股比例(如10送10,则split_ratio=2)
"""
# 复权因子 = (1 + split_ratio) * (1 - dividend / price)
# 实际计算时,需要逐日迭代
pass
# 使用现成的库(推荐)
import tushare as ts
df = ts.get_k_data('000001', start='2024-01-01', end='2024-12-31')
# tushare默认返回前复权数据
# 如果需要后复权,可以自己计算
我的建议:如果你用Python做回测,直接用backtrader或zipline这类框架,它们内置了复权处理。但如果你自己写回测引擎,一定要把复权逻辑写对。我曾经因为复权因子算错,导致回测收益差了20%。
四、收益率序列生成:从价格到收益的转换
价格数据是「非平稳」的,不能直接用来建模。收益率序列才是量化分析的起点。
说白了,我们关心的是「涨了多少」,而不是「现在多少钱」。
常见的收益率计算方式
- 简单收益率:(P_t - P_{t-1}) / P_{t-1}
- 对数收益率:ln(P_t / P_{t-1})
- 累计收益率:从起始点到当前点的总收益
# 计算收益率
df['simple_return'] = df['price'].pct_change()
df['log_return'] = np.log(df['price'] / df['price'].shift(1))
# 计算累计收益率
df['cum_return'] = (1 + df['simple_return']).cumprod() - 1
# 注意:第一行会是NaN,需要处理
df = df.dropna()
重要提醒:对数收益率具有可加性,适合做统计分析。简单收益率具有可乘性,适合计算组合收益。我一般用对数收益率做因子分析,用简单收益率做回测。
五、知识体系总览
下面这张图,是我自己梳理的数据清洗流程。你照着这个顺序做,基本不会漏。
这张图里,每一步都环环相扣。你跳过了缺失值处理直接做复权,复权因子可能算错。你跳过了异常值检测直接算收益率,一个异常点就能让你的夏普比率失真。
最后提醒一句:数据清洗没有「标准答案」。不同的策略、不同的市场、不同的频率,处理方法都不一样。我的建议是——先理解你的数据,再动手清洗。别上来就写代码,先画个图看看数据长什么样。
好了,这一章的内容就这些。数据清洗虽然枯燥,但它是量化投资的基石。你想想看,如果地基没打好,楼盖得再高也是危楼。