第三章 因子数据准备:数据源选择、清洗与中性化处理
做因子投资,第一步就是搞数据。这一步要是没做好,后面再漂亮的模型也是白搭。我见过太多人花大把时间调参数,结果问题出在数据上——嗯,今天我们就来聊聊怎么把数据这关把好。
3.1 数据源选择:选对工具,事半功倍
国内做量化,主流的数据源就三个:Wind、聚宽、Tushare。我三个都用过,说说我的真实感受。
| 数据源 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| Wind | 数据最全、更新最快、机构认可度高 | 贵(一年几万)、需要Windows环境 | 机构实盘、专业研究 |
| 聚宽 | 本地化好、API简洁、免费额度够用 | 历史数据有限、部分字段缺失 | 个人研究、策略回测 |
| Tushare | 开源免费、社区活跃、数据种类多 | 稳定性一般、积分限制 | 学习入门、小规模研究 |
我的建议:如果你刚开始做因子研究,先用聚宽或Tushare跑通流程。等策略成熟了,再上Wind。我在项目中遇到过用Tushare跑回测结果很好,换到Wind数据后因子IC直接腰斩的情况——数据源差异真的很大。
3.2 数据清洗:去极值与标准化
拿到原始数据后,第一件事就是清洗。说白了,就是要把那些明显不对劲的数据处理掉。
3.2.1 去极值(Winsorization)
为什么要去极值?你想想看,一个因子如果有个别股票的值特别大,比如市盈率突然变成1000倍,那整个因子的分布就被带偏了。我习惯用MAD(中位数绝对偏差)方法,比3σ更稳健。
import numpy as np
import pandas as pd
def winsorize_mad(series, n=5):
"""
基于MAD的去极值处理
n: 阈值倍数,我一般取5
"""
median = series.median()
mad = np.median(np.abs(series - median))
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
# 使用示例
factor_data['pe_ratio'] = winsorize_mad(factor_data['pe_ratio'])
注意:去极值不是删除数据,而是把极端值拉到边界。我曾经犯过直接删除极端值的错误,结果导致样本偏差,回测结果虚高。后来才明白,极端值本身也是信息,只是需要合理处理。
3.2.2 标准化(Standardization)
标准化是为了让不同量纲的因子可以放在一起比较。常用的有两种:Z-score和Rank标准化。
def standardize_zscore(series):
"""Z-score标准化"""
return (series - series.mean()) / series.std()
def standardize_rank(series):
"""Rank标准化,转为[0,1]区间"""
return series.rank(pct=True)
我个人更推荐Rank标准化。为什么?因为Z-score假设数据服从正态分布,但实际因子数据往往有偏。Rank标准化不依赖分布假设,而且对异常值更鲁棒。我在做多因子合成时,用Rank标准化的效果明显好于Z-score。
3.3 行业市值中性化处理
这一步是因子研究的核心环节。说白了,就是要把因子中跟行业和市值相关的部分剥离掉,只保留纯因子收益。
3.3.1 为什么要中性化?
你想想看,如果某个因子在银行股上表现好,在科技股上表现差,那这个因子的收益到底是因子本身的贡献,还是行业配置的贡献?中性化就是为了回答这个问题。
3.3.2 怎么做中性化?
最常用的方法是回归法。把因子值对行业哑变量和市值做回归,取残差作为中性化后的因子。
import statsmodels.api as sm
def neutralize_factor(factor_df, industry_dummies, market_cap):
"""
行业市值中性化
factor_df: 因子值
industry_dummies: 行业哑变量(one-hot编码)
market_cap: 市值(取对数)
"""
# 准备自变量
X = pd.concat([industry_dummies, np.log(market_cap)], axis=1)
X = sm.add_constant(X)
# 回归取残差
model = sm.OLS(factor_df, X).fit()
residual = factor_df - model.predict(X)
return residual
关键点:中性化后的因子值,代表的是「剔除行业和市值影响后」的纯因子信号。这个信号才能用来构建多空组合。
3.4 数据准备的整体流程
我把整个流程画了张图,方便你理解各个环节的关系。
3.5 实战中的坑与经验
做了这么多年因子研究,我踩过不少坑。分享几个最典型的:
- 数据对齐问题:不同数据源的股票代码格式不一样,Wind是WIND_CODE,聚宽是JQ_CODE。我建议统一用股票代码+市场标识,比如"000001.SZ"这种格式。
- 停牌处理:停牌股票的因子值要特别处理。我一般会把停牌超过20天的股票直接剔除,不然会引入幸存者偏差。
- 新上市股票:上市不满60天的股票,因子值往往不稳定。我习惯把它们排除在样本外。
- 行业分类标准:申万一级、中信一级、GICS,不同分类标准结果差异很大。我个人推荐用申万一级,因为覆盖面广,更新及时。
避坑指南:我曾经在回测时发现因子收益突然变差,查了两天才发现是行业分类标准变了——申万在2021年更新了行业分类,老代码和新代码不兼容。从那以后,我每次都会检查行业分类的版本号。
3.6 代码实战:完整的数据准备流程
下面是一个完整的示例,把今天讲的内容串起来。
import pandas as pd
import numpy as np
from scipy import stats
def prepare_factor_data(factor_df, industry_col='industry', mkt_cap_col='mkt_cap'):
"""
完整的因子数据准备流程
"""
# 1. 去极值(MAD方法)
for col in factor_df.columns:
if col not in [industry_col, mkt_cap_col]:
factor_df[col] = winsorize_mad(factor_df[col])
# 2. 标准化(Rank标准化)
for col in factor_df.columns:
if col not in [industry_col, mkt_cap_col]:
factor_df[col] = standardize_rank(factor_df[col])
# 3. 行业市值中性化
industry_dummies = pd.get_dummies(factor_df[industry_col])
neutralized = neutralize_factor(
factor_df.drop(columns=[industry_col, mkt_cap_col]),
industry_dummies,
factor_df[mkt_cap_col]
)
return neutralized
# 使用示例
raw_data = pd.DataFrame({
'pe_ratio': [15, 20, 1000, 18, 22],
'pb_ratio': [1.5, 2.0, 0.8, 1.8, 2.2],
'industry': ['银行', '科技', '银行', '消费', '科技'],
'mkt_cap': [1000, 500, 2000, 800, 300]
})
clean_data = prepare_factor_data(raw_data)
print(clean_data.head())
嗯,数据准备这块就讲到这里。记住一句话:好的因子研究,七分在数据,三分在模型。把数据搞干净了,后面的工作才能站得住脚。