第三章 数据获取与清洗:金融数据源介绍、Pandas数据处理、缺失值处理、极端值处理、行业市值中性化
做量化研究,有一句老话叫「垃圾进,垃圾出」。你因子逻辑再漂亮,回测框架再牛,数据一塌糊涂,结果就是自欺欺人。我见过太多人花几个月挖因子,最后发现是数据没洗干净——那种感觉,比亏钱还难受。
这一章,咱们就聊聊数据那些事。从哪拿数据,拿到后怎么处理,遇到坑怎么填。嗯,都是实战中摸爬滚打出来的经验。
3.1 金融数据源:你的弹药库
先说说数据源。国内常用的就那么几个,我按个人偏好排个序:
| 数据源 | 特点 | 适合场景 | 我的评价 |
|---|---|---|---|
| Wind | 最全,但贵 | 机构实盘 | 行业标准,没它不行 |
| 聚宽/JQData | API友好,Python原生 | 回测研究 | 个人最爱,上手快 |
| Tushare | 免费,社区活跃 | 学习入门 | 良心项目,但稳定性一般 |
| RiceQuant | 云端策略 | 快速验证 | 适合不想搭环境的人 |
我个人习惯用聚宽的数据接口。为什么?因为它直接返回DataFrame,省去了我写解析代码的时间。你想想看,每天处理几百个因子,如果每个数据源都要写一套解析逻辑,那得累死。
3.2 Pandas数据处理:你的瑞士军刀
拿到数据后,第一件事是什么?不是跑因子,是看数据长什么样。我每次拿到新数据,都会先跑这几行代码:
import pandas as pd
import numpy as np
# 假设df是日频行情数据
print(df.shape) # 看看有多大
print(df.head()) # 瞄一眼前几行
print(df.info()) # 检查数据类型和缺失情况
print(df.describe()) # 看分布统计
这一步看似简单,但能帮你发现很多问题。比如某列全是NaN,或者日期格式不对,或者价格出现了负数——嗯,这些我都遇到过。
接下来是数据对齐。因子研究里,不同数据源的频率可能不一样。有的日频,有的月频,有的甚至是不规则频率。我的做法是统一用交易日历做索引:
# 对齐到交易日历
trade_cal = get_trade_days(start_date, end_date)
df = df.reindex(trade_cal)
为什么要这么做?因为金融数据天然有缺失——周末、节假日都没数据。如果你不显式对齐,pandas会给你搞出很多奇怪的索引对齐问题。我曾经因为这个bug排查了整整两天,最后发现是索引没对齐。
3.3 缺失值处理:别让NaN毁了你的因子
缺失值,说白了就是数据没拿到。原因很多:停牌、新股上市、数据源抽风……
处理缺失值,我一般分三步走:
- 先诊断:看看缺失比例。超过50%的列,直接扔掉。
- 再分类:是随机缺失还是系统性缺失?停牌导致的缺失是系统性的,需要特殊处理。
- 最后填充:常用的方法有向前填充、插值、用行业均值填充。
代码示例:
# 第一步:检查缺失比例
missing_ratio = df.isnull().mean()
cols_to_drop = missing_ratio[missing_ratio > 0.5].index
df.drop(columns=cols_to_drop, inplace=True)
# 第二步:向前填充(适合停牌数据)
df.fillna(method='ffill', inplace=True)
# 第三步:剩余缺失用行业均值填充
df['factor'] = df.groupby('industry')['factor'].transform(
lambda x: x.fillna(x.mean())
)
3.4 极端值处理:别让异常值带偏你的模型
极端值,就是那些离谱的数据。比如某天某只股票涨了1000%,或者市盈率突然变成负数——这些基本都是数据错误,或者是一些特殊事件(比如新股上市首日)。
处理极端值,我常用的方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| MAD法 | 基于中位数绝对偏差 | 因子分布不对称时 |
| 百分位法 | 截断上下1%/5% | 快速清洗,简单粗暴 |
| 标准差法 | 超过3倍标准差截断 | 正态分布假设下 |
我个人偏好MAD法。为什么?因为它对异常值更鲁棒。标准差法受极端值影响太大——你想想看,如果数据里有一个极端大的值,标准差会被拉得很大,结果就是正常值反而被截断了。
代码实现:
def winsorize_mad(series, n=5):
"""基于MAD的缩尾处理"""
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
# 应用
df['factor_winsorized'] = df.groupby('date')['factor'].transform(winsorize_mad)
注意这里我按日期分组处理。为什么?因为不同时间点的因子分布可能不同。牛市和熊市,因子的波动范围差远了。
3.5 行业市值中性化:让因子「纯净」起来
这一步,是因子研究的核心环节。说白了,就是剔除行业和市值对因子的影响。
为什么要做中性化?举个例子:你发现小市值股票表现好,但这可能只是因为小市值因子在起作用,而不是你挖的那个因子本身有效。中性化就是要把这种「搭便车」的影响去掉。
做法很简单:用因子值对行业虚拟变量和市值做回归,取残差作为新的因子值。
import statsmodels.api as sm
def neutralize_factor(df, factor_col, industry_col, market_cap_col):
"""行业市值中性化"""
# 创建行业虚拟变量
industry_dummies = pd.get_dummies(df[industry_col], prefix='ind')
# 构建回归模型
X = pd.concat([industry_dummies, df[[market_cap_col]]], axis=1)
X = sm.add_constant(X)
y = df[factor_col]
# 回归取残差
model = sm.OLS(y, X).fit()
df['factor_neutral'] = model.resid
return df
我曾经犯过一个错误:在全体数据上做了一次中性化,结果因子效果看起来特别好。后来才发现,因为不同时期的市值分布不同,这种做法相当于「偷看了未来数据」。嗯,那次的教训挺深刻的。
知识体系总览
下面这张图,把本章的核心逻辑串起来了:
数据清洗这件事,说难不难,说简单也不简单。难的是你永远不知道下一个坑在哪。我做了这么多年,每次拿到新数据还是会小心翼翼。记住一句话:对数据保持敬畏,它就不会坑你。