3. 数据采集与清洗:从原始数据到可用因子
数据采集与清洗,说白了就是给因子模型「喂饭」。饭要是馊了,模型再牛也白搭。我在量化行业这些年,见过太多团队花90%的时间调模型,却只花10%的时间管数据——结果回测漂亮得像神话,实盘一跑就现原形。
今天咱们就聊聊,怎么把这顿饭做干净、做标准。
3.1 数据采集策略:别什么都往锅里倒
数据采集不是「越多越好」。我有个血的教训:刚入行时,我恨不得把全市场所有能爬的数据都存下来,结果存储成本翻了三倍,真正用上的不到20%。
所以,采集之前先问自己三个问题:
- 这个数据对因子逻辑有贡献吗? 比如你做一个动量因子,那财报数据就不是必须的。
- 数据源的可靠性如何? 免费API的数据,你敢直接上实盘?
- 采集频率够用吗? 日频策略用分钟数据,纯属浪费资源。
我个人习惯把数据源分成三个梯队:
| 梯队 | 数据源 | 用途 | 更新频率 |
|---|---|---|---|
| 第一梯队 | 交易所直连、Wind、Bloomberg | 核心因子计算 | 实时/日频 |
| 第二梯队 | 第三方数据商(如通联、聚宽) | 辅助验证、回测 | 日频/周频 |
| 第三梯队 | 爬虫数据、新闻舆情 | 另类因子探索 | 按需 |
3.2 数据清洗流程:把脏东西筛出去
数据清洗,我把它拆成三步:去重、异常值处理、缺失值填充。顺序不能乱,乱了你后面会多花三倍时间。
3.2.1 去重:别让同一笔数据算两遍
数据重复的原因很多:网络重传、API重复调用、数据商推送了修正数据但没覆盖旧的。我曾经在回测中发现一个因子夏普比异常高,查了两天才发现——某只股票的数据被重复插入了三次,相当于凭空多了两倍的交易量。
去重的核心逻辑很简单:
# 按股票代码 + 日期 + 数据源去重
df.drop_duplicates(subset=['stock_code', 'trade_date', 'source'], keep='last')
这里有个坑:keep='last' 意味着保留最新的一条。为什么?因为数据商推送的修正数据通常比原始数据更准。你想想看,如果原始数据错了,你保留旧的,那修正就白费了。
3.2.2 异常值处理:别让极端值带偏模型
异常值分两种:一种是「明显错误」,比如股票价格出现负数;另一种是「极端但可能真实」,比如某只小盘股一天涨了50%。
我的处理原则是:
- 明显错误:直接剔除或置为NaN。比如价格 <= 0,成交量 <= 0。
- 极端值:用MAD(中位数绝对偏差)或3σ方法识别,然后做缩尾处理(Winsorize)。
举个例子:
# 3σ 方法识别异常值
mean = df['return'].mean()
std = df['return'].std()
df['is_outlier'] = (df['return'] - mean).abs() > 3 * std
# 缩尾处理:把异常值拉到上下限
upper = mean + 3 * std
lower = mean - 3 * std
df['return'] = df['return'].clip(lower, upper)
3.2.3 缺失值填充:别让空值毁了你的因子
缺失值处理,没有银弹。我见过有人一股脑全用0填充,结果因子分布完全扭曲。也见过有人用线性插值,结果在停牌期间插出了「幽灵价格」。
我的建议是分情况讨论:
| 缺失类型 | 推荐方法 | 原因 |
|---|---|---|
| 随机缺失(如网络波动) | 前向填充(ffill) | 价格、成交量等序列数据,用上一个有效值最合理 |
| 停牌导致的缺失 | 保留NaN,或填充停牌前价格 | 停牌期间没有交易,填充任何值都是错的 |
| 财报数据缺失 | 用行业均值填充 | 财报数据有周期性,行业均值比0更合理 |
| 大量连续缺失(>20%) | 直接剔除该股票 | 数据质量太差,留着也是噪音 |
3.3 数据标准化:让不同量纲的数据能一起玩
标准化,说白了就是把不同尺度的数据拉到同一个量级。比如市盈率可能是几十,换手率可能是百分之几,不标准化的话,模型会天然偏向数值大的特征。
常用的方法有三种:
- Z-score标准化:减去均值,除以标准差。适合数据近似正态分布的情况。
- Min-Max归一化:缩放到[0,1]区间。适合有明确上下界的数据。
- Rank标准化:把数据排序后映射到[0,1]。我最喜欢这个——它对异常值不敏感,而且能保留数据的排序信息。
代码实现:
# Rank标准化
def rank_standardize(series):
rank = series.rank()
return (rank - 1) / (len(series) - 1)
# 注意:一定要在截面(cross-section)上做标准化
# 即每个时间点,对所有股票做一次标准化
df['factor_standardized'] = df.groupby('trade_date')['factor_raw'].apply(rank_standardize)
3.4 数据对齐:让不同频率的数据对上号
数据对齐,是数据清洗里最容易被忽视的一环。你想想看,日频的收盘价和月频的财报数据,怎么放到同一个模型里?
我的做法是:
- 高频向低频对齐:比如把分钟数据聚合成日频数据。聚合方式要谨慎——用收盘价还是均价?用最高价还是VWAP?
- 低频向高频对齐:比如把财报数据填充到每个交易日。这里要注意「未来信息」的问题——财报发布日之前,你不能用这个数据。
举个例子:
# 财报数据对齐到交易日
# 假设财报数据有 'report_date'(财报截止日)和 'announce_date'(发布日期)
# 对齐时,只能用 announce_date 之前的数据
# 错误做法:直接用 report_date 对齐
# 正确做法:用 announce_date 做前向填充
df_trade['pe_ttm'] = df_financial.reindex(df_trade.index, method='ffill')
3.5 本章知识体系
下面这张图,是我自己总结的数据采集与清洗流程。每次做新项目,我都会拿出来对照一遍:
嗯,这张图基本概括了咱们今天讲的所有内容。从数据采集开始,经过去重、异常值处理、缺失值填充,再到标准化和对齐,最后输出可用的因子数据。每一步都有坑,但只要你按这个流程走,至少能保证数据质量在及格线以上。
最后说一句:数据治理不是一次性工作。我建议你每周跑一次数据质量报告,看看缺失率、异常率有没有变化。数据会「变质」的——数据商可能改接口,交易所可能调整规则,你不盯着,它就会悄悄出问题。