数据准备与清洗:从原始数据到可用因子
做因子研究这么多年,我最大的体会就是:数据准备占了80%的工作量,却决定了100%的结果。你想想看,再牛的模型,喂进去的是垃圾数据,出来的只能是垃圾结论。这一章,我就把数据准备与清洗的实战经验掰开揉碎了讲给你听。
数据源选择:Wind、Tushare、JoinQuant怎么选?
我个人习惯同时备2-3个数据源。为什么?因为单一数据源总有坑。我曾在Wind上拿到过某只股票的复权数据,跟Tushare对不上,查了半天才发现是复权方式不同。
三个主流数据源的特点:
| 数据源 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| Wind | 数据最全,财务数据权威 | 贵,接口慢,本地化麻烦 | 机构实盘、回测验证 |
| Tushare | 免费,社区活跃,文档清晰 | 有调用次数限制,部分数据延迟 | 个人研究、快速原型 |
| JoinQuant | 自带回测框架,数据预处理完善 | 封闭生态,数据导出受限 | 策略开发、快速验证 |
我的建议:初期用Tushare做研究,中期用JoinQuant做回测,最终用Wind做实盘校验。三个数据源交叉验证,能过滤掉大部分数据问题。
数据对齐:时间与标的的精准匹配
数据对齐是个看似简单、实则坑极多的环节。说白了,就是让不同数据在时间和标的上「对上号」。
我遇到过最典型的问题:某只股票在2018年6月停牌了,但财务数据还在更新。如果你不做对齐,因子值就会「穿越」到停牌期间,产生虚假信号。
对齐的核心步骤:
- 时间对齐:统一使用交易日历,剔除非交易日
- 标的对齐:确保所有因子在同一时间点对应同一只股票
- 频率对齐:日频数据不要混入周频数据,反之亦然
# 一个简单的对齐示例
import pandas as pd
def align_data(factor_df, price_df, trade_calendar):
"""
将因子数据与价格数据对齐到交易日历
"""
# 只保留交易日
common_dates = factor_df.index.intersection(price_df.index)
common_dates = common_dates.intersection(trade_calendar)
# 对齐标的
common_stocks = factor_df.columns.intersection(price_df.columns)
return factor_df.loc[common_dates, common_stocks]
注意:千万不要直接用merge或join粗暴对齐。我曾经这么干过,结果把停牌期间的数据也混进去了,回测曲线漂亮得不像话,实盘直接打脸。
缺失值处理:别让NaN毁了你的因子
缺失值处理,说白了就是「填坑」。但怎么填,很有讲究。
我常用的三种方法:
- 删除法:缺失比例超过50%的因子直接扔掉。干净利落,但会损失信息。
- 填充法:用行业均值、中位数或前向填充。我个人偏爱中位数填充,因为它不受极端值影响。
- 标记法:把缺失本身作为一个特征。比如新增一列「is_missing」,值为0或1。
# 缺失值处理实战
def handle_missing(factor_df, method='median'):
if method == 'median':
# 按行业分组填充中位数
return factor_df.groupby('industry').transform(
lambda x: x.fillna(x.median())
)
elif method == 'forward':
# 前向填充(适用于时间序列)
return factor_df.fillna(method='ffill')
一个小技巧:对于刚上市不足60天的股票,很多因子数据是缺失的。我一般直接剔除这些标的,因为它们的「历史太短」,统计意义不大。
异常值处理:MAD法与百分位法
异常值处理,是因子有效性检验中最容易被忽视的环节。你想想看,一个极端值就能把整个相关系数拉偏。
我常用的两种方法:
MAD法(中位数绝对偏差法)
MAD法的核心思想:用中位数代替均值,用MAD代替标准差。为什么?因为中位数对异常值不敏感。
def mad_winsorize(series, n=5):
"""
MAD法去极值
n=5 表示超出5倍MAD的值被截断
"""
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
百分位法
百分位法更直观:直接砍掉头尾各1%或5%的数据。我一般用1%和99%分位数做截断。
def percentile_winsorize(series, lower=0.01, upper=0.99):
"""
百分位法去极值
"""
q_low = series.quantile(lower)
q_high = series.quantile(upper)
return series.clip(q_low, q_high)
我的经验:MAD法更适合金融数据,因为金融数据往往有厚尾分布。百分位法虽然简单,但会丢失尾部信息。我一般先用MAD法,如果效果不好再换百分位法。
中性化处理:剥离行业与市值的影响
中性化处理,说白了就是「剔除干扰因素」。你研究的是因子本身的有效性,而不是行业或市值带来的噪音。
我常用的方法是回归中性化:
- 将因子值对行业哑变量和市值做回归
- 取回归的残差作为中性化后的因子值
import statsmodels.api as sm
def neutralize_factor(factor, industry_dummies, market_cap):
"""
对因子做行业和市值中性化
"""
X = pd.concat([industry_dummies, np.log(market_cap)], axis=1)
X = sm.add_constant(X)
model = sm.OLS(factor, X).fit()
return model.resid # 残差就是中性化后的因子
曾经踩过的坑:有一次我忘了对市值取对数,结果中性化后的因子跟市值还有很强的相关性。取对数后,这个问题就解决了。嗯,细节决定成败。
本章知识体系
下面这张图,是我梳理的数据准备与清洗的完整流程。你可以把它当作一个检查清单:
数据准备与清洗,说白了就是「磨刀不误砍柴工」。我见过太多人急着跑模型,结果数据没处理好,白白浪费了时间。记住:好的数据,是因子有效性检验的基石。