第三章 因子数据准备:数据源选择、清洗与中性化处理

做因子投资,第一步就是搞数据。这一步要是没做好,后面再漂亮的模型也是白搭。我见过太多人花大把时间调参数,结果问题出在数据上——嗯,今天我们就来聊聊怎么把数据这关把好。

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 数据准备的整体流程

我把整个流程画了张图,方便你理解各个环节的关系。

因子数据准备流程 数据源选择 Wind / 聚宽 / Tushare 去极值处理 MAD方法 / 3σ方法 标准化 Z-score / Rank 行业市值中性化 回归法取残差 纯因子信号 用于多空组合构建 注意:中性化必须在标准化之后进行 否则行业和市值的影响会干扰标准化结果

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())

嗯,数据准备这块就讲到这里。记住一句话:好的因子研究,七分在数据,三分在模型。把数据搞干净了,后面的工作才能站得住脚。