第二章:数据准备与清洗
做量化策略,有一句话我特别认同:垃圾进,垃圾出。模型再牛,参数再优,数据不行,一切都是白搭。这一章,咱们就聊聊数据准备与清洗的那些事儿。
核心观点:数据清洗占整个因子开发工作量的60%以上,别嫌烦,这是基本功。
2.1 数据源选择:别在源头就输了
我个人习惯,数据源至少要备两个。一个主用,一个备用。为什么?因为数据源出问题,太常见了。
国内常用的数据源,我列个表给你看:
| 数据源 | 优点 | 缺点 | 我的建议 |
|---|---|---|---|
| Wind | 覆盖面广,机构认可度高 | 贵,个人用户不友好 | 机构首选 |
| Tushare | 免费,社区活跃 | 稳定性一般,有调用限制 | 个人研究够用 |
| 聚宽/JQData | API友好,文档清晰 | 部分数据需要积分 | 量化入门推荐 |
| AkShare | 开源免费,数据种类多 | 接口变动频繁 | 适合爬虫能力强的 |
我在项目中遇到过,某次用Tushare拉日线数据,突然某天接口返回全为空。幸好我同时备了聚宽的数据,才没耽误回测。所以,永远不要只依赖一个数据源。
小技巧:建议用Wind或聚宽作为主力数据源,Tushare或AkShare作为校验和补充。每次拉完数据,交叉验证一下关键字段,比如收盘价、成交量。
2.2 数据对齐:时间轴必须统一
多因子模型,说白了就是把不同来源的数据拼在一起。但问题来了——股票A有3000个交易日,股票B只有2800个,因子C的数据频率是日频,因子D是周频。怎么对齐?
我一般这么做:
- 确定基准时间轴:通常以交易日期为基准,剔除非交易日。
- 统一频率:所有数据都对齐到日频。周频数据,我习惯取每周最后一个交易日。
- 处理停牌:停牌期间的数据,用前值填充,或者直接设为NaN。
代码实现其实不复杂:
import pandas as pd
# 假设df1是股票日线,df2是因子数据
# 先确保日期列是datetime类型
df1['trade_date'] = pd.to_datetime(df1['trade_date'])
df2['date'] = pd.to_datetime(df2['date'])
# 合并时用outer join,保留所有日期
merged = pd.merge(df1, df2, left_on=['trade_date', 'stock_code'],
right_on=['date', 'stock_code'], how='outer')
# 按股票和日期排序
merged.sort_values(['stock_code', 'trade_date'], inplace=True)
注意:千万不要用inner join!否则那些停牌日、非交易日的数据会被直接丢掉,回测结果会严重失真。我曾经因为这个坑,回测收益虚高了20%。
2.3 缺失值处理:填还是不填?
这是个经典问题。我的原则很简单:能填就填,不能填就扔。
具体来说:
- 缺失率 < 5%:用前向填充(ffill)或中位数填充。我个人偏好中位数,因为它不受极端值影响。
- 缺失率 5%-20%:用行业均值填充。比如某只股票的市盈率缺失,就用同行业其他股票的市盈率中位数来填。
- 缺失率 > 20%:直接删除这个因子或这只股票。别犹豫,数据质量太差,留着也是噪音。
举个例子:
# 前向填充
df['factor'] = df.groupby('stock_code')['factor'].ffill()
# 行业均值填充
df['factor'] = df.groupby('industry')['factor'].transform(
lambda x: x.fillna(x.median())
)
嗯,这里要注意:千万不要用全局均值填充。为什么?因为不同行业的因子分布差异很大,全局均值会抹平行业特征,导致因子失效。我刚开始做的时候犯过这个错,后来发现因子IC值怎么调都不对,查了半天才发现是填充方式的问题。
2.4 异常值检测与处理:别让一颗老鼠屎坏了一锅粥
异常值,说白了就是那些离谱的数据。比如某股票市盈率突然变成10000倍,或者成交量突然放大100倍。这些数据如果不处理,模型会被带偏。
我常用的方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 3σ原则 | 超过均值±3倍标准差视为异常 | 数据近似正态分布 |
| IQR方法 | 超过Q1-1.5IQR或Q3+1.5IQR视为异常 | 数据分布偏态 |
| MAD方法 | 基于中位数绝对偏差,更稳健 | 存在较多极端值 |
我个人最常用的是MAD方法,因为它对极端值不敏感。代码实现:
import numpy as np
def mad_detection(series, threshold=3.5):
median = np.median(series)
mad = np.median(np.abs(series - median))
modified_z_scores = 0.6745 * (series - median) / mad
return np.abs(modified_z_scores) > threshold
# 应用
df['is_outlier'] = df.groupby('trade_date')['factor'].transform(
lambda x: mad_detection(x)
)
df.loc[df['is_outlier'], 'factor'] = np.nan # 设为缺失,后续填充
避坑指南:我曾经直接用3σ原则处理所有因子,结果发现有些因子的分布本身就是尖峰厚尾的,3σ会误删很多正常数据。后来我改用MAD方法,效果好了很多。记住:没有万能的方法,要根据数据分布特点来选择。
2.5 数据标准化:让因子站在同一起跑线
不同因子的量纲差异很大。比如市盈率可能是几倍到几百倍,换手率可能是0.1%到10%。如果不标准化,模型会天然偏向数值大的因子。
常用的标准化方法:
- Z-score标准化:(x - mean) / std。适用于数据近似正态分布。
- Min-Max标准化:(x - min) / (max - min)。将数据映射到[0,1]区间。
- Rank标准化:将数据排序后转为百分位数。对异常值最稳健。
我个人的经验是:做截面标准化,不要做时间序列标准化。什么意思?就是每个交易日,对所有股票的因子值进行标准化,而不是对某只股票的历史数据做标准化。因为截面标准化能保留横截面上的相对强弱信息,这才是多因子模型需要的。
# 截面Z-score标准化
df['factor_zscore'] = df.groupby('trade_date')['factor'].transform(
lambda x: (x - x.mean()) / x.std()
)
# 截面Rank标准化
df['factor_rank'] = df.groupby('trade_date')['factor'].transform(
lambda x: x.rank(pct=True)
)
重要提醒:标准化一定要在训练集上计算参数,然后应用到测试集。千万不要在整个数据集上做标准化,否则会引入未来信息,导致回测结果虚高。这个坑我踩过,回测收益漂亮得不行,实盘直接打脸。
本章知识体系
下面这张图,帮你梳理数据准备与清洗的完整流程:
数据准备与清洗,说白了就是给模型喂好料。这一步做扎实了,后面的因子检验和模型训练才能事半功倍。别嫌麻烦,我见过太多人在这上面偷懒,结果回测和实盘差距巨大,最后还得回头重新清洗数据。
记住一句话:数据质量决定了策略收益的上限,模型只是逼近这个上限的工具。