4. 数据准备与清洗:获取行情数据、财务数据、因子数据,处理缺失值、异常值、幸存者偏差
做量化的人都知道一句话:垃圾进,垃圾出。模型再漂亮,数据一塌糊涂,结果就是自欺欺人。我见过太多人花几个月调参数,最后发现是数据没洗干净——那种感觉,就像你跑完马拉松才发现鞋里全是沙子。
这一章,咱们就聊聊数据准备这件事。说白了,就是怎么把原始数据变成能喂给模型吃的干净食材。
4.1 数据来源:行情、财务、因子
先说说数据从哪来。我个人习惯把数据分成三大类:
- 行情数据:开盘价、收盘价、最高价、最低价、成交量、成交额。这是最基础的,也是频率最高的。
- 财务数据:营收、净利润、ROE、资产负债率等。季度更新,频率低但信息量大。
- 因子数据:基于行情和财务加工出来的衍生指标,比如动量因子、价值因子、质量因子。
你想想看,这三类数据就像盖房子的砖、水泥和钢筋。缺了哪样,房子都不稳。
核心原则:数据源要稳定、可追溯。我建议至少准备两个独立的数据源做交叉验证。曾经有一次,我发现某个数据源的复权价格算错了,要不是有另一个源做对比,那整个回测就废了。
4.2 数据清洗:缺失值处理
缺失值,是量化里最烦人的问题之一。为什么会有缺失?原因很多:停牌、数据源漏了、新股上市第一天没有历史数据……
处理方式,我一般分三步走:
- 识别缺失模式:是随机缺失,还是系统性缺失?比如ST股经常缺失财务数据,这就是系统性的。
- 选择填充方法:
- 前向填充(ffill):用上一个交易日的数据填充。适合行情数据。
- 后向填充(bfill):用下一个交易日的数据填充。慎用,容易引入未来信息。
- 插值法:线性插值、多项式插值。适合财务数据这种低频序列。
- 行业均值填充:用同行业其他公司的均值替代。适合因子数据。
- 标记缺失:我习惯在数据里加一列缺失标记,方便后续分析。
我的经验:千万别一股脑全填充。我曾经在回测里用后向填充处理停牌数据,结果策略表现好得离谱——后来才发现,停牌复牌后的跳空被当成了策略收益。嗯,那叫一个尴尬。
4.3 异常值处理
异常值,就是那些明显偏离正常范围的数据点。比如某只股票一天涨了1000%,或者市盈率突然变成负数的一万倍。
为什么会这样?数据录入错误、除权除息没处理好、极端市场事件……原因很多。
我常用的方法:
- MAD法:中位数绝对偏差。比标准差更稳健,不受极端值影响。
- 分位数截断:比如把上下1%的数据截掉。简单粗暴,但有效。
- 业务逻辑判断:比如市盈率不可能为负(除非亏损),涨跌幅不可能超过100%(A股涨停板限制)。
注意:异常值不一定是错误。比如2015年股灾时,很多股票连续跌停,那确实是真实数据。所以处理异常值之前,先问问自己:这个异常是数据问题,还是市场真实情况?
4.4 幸存者偏差:最隐蔽的坑
幸存者偏差,说白了就是你只看到了活下来的,忽略了死掉的。
举个例子:你回测一个选股策略,用的数据是今天还在交易的股票。但那些退市的、被ST的、跌成渣的股票呢?它们的数据被你自动过滤掉了。结果就是,你的策略看起来特别牛,因为只选了「幸存者」。
我刚开始做量化时,就踩过这个坑。回测年化收益30%,实盘一跑直接亏成狗。后来才发现,数据里全是活到现在的股票,那些退市的垃圾股根本没参与回测。
怎么避免?
- 使用全量数据:包括已经退市的、被并购的、暂停上市的股票。
- 动态回测:在回测的每一天,只使用当时真实存在的股票。
- 加入退市标记:在数据里标记哪些股票后来退市了,方便做敏感性分析。
一句话总结:回测时,你只能知道过去的信息。别让未来的信息偷偷溜进你的数据里。
4.5 数据清洗流程框架
下面这张图,是我自己总结的数据清洗流程。每次做新项目,我都会按这个框架走一遍。
4.6 代码示例:数据清洗实战
光说不练假把式。下面这段代码,是我在项目中常用的数据清洗流程。你直接拿去用,改改参数就行。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('stock_data.csv', index_col='date', parse_dates=True)
# 1. 缺失值处理
# 前向填充(行情数据)
df['close'] = df['close'].fillna(method='ffill')
# 行业均值填充(因子数据)
df['pe_ratio'] = df.groupby('industry')['pe_ratio'].transform(
lambda x: x.fillna(x.mean())
)
# 2. 异常值处理
# MAD法
def mad_outlier(series, threshold=3):
median = series.median()
mad = np.abs(series - median).median()
return np.abs(series - median) / mad > threshold
df = df[~mad_outlier(df['return_1d'])]
# 3. 幸存者偏差处理
# 确保包含退市股票
df = df[df['is_alive'] == True] # 回测时只取当时存活的股票
print('数据清洗完成,剩余记录数:', len(df))
小技巧:我习惯把清洗逻辑封装成一个函数,每次跑新数据直接调用。这样既省时间,又不容易出错。
4.7 数据质量检查清单
最后,分享一个我每次做数据清洗都会检查的清单。你可以打印出来贴在工位上。
| 检查项 | 说明 | 检查方法 |
|---|---|---|
| 缺失率 | 每个字段的缺失比例 | df.isnull().mean() |
| 异常值 | 是否存在极端值 | 描述性统计 + 箱线图 |
| 幸存者偏差 | 是否包含退市股票 | 检查股票数量是否随时间递减 |
| 未来信息 | 是否使用了未来数据 | 检查填充方法是否引入了未来值 |
| 数据一致性 | 不同数据源是否一致 | 交叉验证 |
数据准备这件事,看起来枯燥,但它是整个量化策略的基石。我见过太多人在这上面翻车,包括我自己。所以,别嫌麻烦,把数据洗干净了再上模型。
记住一句话:数据干净了,策略就成功了一半。