数据准备与清洗:从原始数据到可用因子

做因子研究这么多年,我最大的体会就是:数据准备占了80%的工作量,却决定了100%的结果。你想想看,再牛的模型,喂进去的是垃圾数据,出来的只能是垃圾结论。这一章,我就把数据准备与清洗的实战经验掰开揉碎了讲给你听。

数据源选择:Wind、Tushare、JoinQuant怎么选?

我个人习惯同时备2-3个数据源。为什么?因为单一数据源总有坑。我曾在Wind上拿到过某只股票的复权数据,跟Tushare对不上,查了半天才发现是复权方式不同。

三个主流数据源的特点:

数据源 优势 劣势 适合场景
Wind 数据最全,财务数据权威 贵,接口慢,本地化麻烦 机构实盘、回测验证
Tushare 免费,社区活跃,文档清晰 有调用次数限制,部分数据延迟 个人研究、快速原型
JoinQuant 自带回测框架,数据预处理完善 封闭生态,数据导出受限 策略开发、快速验证
我的建议:初期用Tushare做研究,中期用JoinQuant做回测,最终用Wind做实盘校验。三个数据源交叉验证,能过滤掉大部分数据问题。

数据对齐:时间与标的的精准匹配

数据对齐是个看似简单、实则坑极多的环节。说白了,就是让不同数据在时间和标的上「对上号」。

我遇到过最典型的问题:某只股票在2018年6月停牌了,但财务数据还在更新。如果你不做对齐,因子值就会「穿越」到停牌期间,产生虚假信号。

对齐的核心步骤:

  1. 时间对齐:统一使用交易日历,剔除非交易日
  2. 标的对齐:确保所有因子在同一时间点对应同一只股票
  3. 频率对齐:日频数据不要混入周频数据,反之亦然
# 一个简单的对齐示例
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法,如果效果不好再换百分位法。

中性化处理:剥离行业与市值的影响

中性化处理,说白了就是「剔除干扰因素」。你研究的是因子本身的有效性,而不是行业或市值带来的噪音。

我常用的方法是回归中性化

  1. 将因子值对行业哑变量和市值做回归
  2. 取回归的残差作为中性化后的因子值
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  # 残差就是中性化后的因子
曾经踩过的坑:有一次我忘了对市值取对数,结果中性化后的因子跟市值还有很强的相关性。取对数后,这个问题就解决了。嗯,细节决定成败。

本章知识体系

下面这张图,是我梳理的数据准备与清洗的完整流程。你可以把它当作一个检查清单:

数据准备与清洗流程 数据源选择 数据对齐 缺失值处理 异常值处理 中性化处理 关键要点 • Wind/Tushare/JQ • 交易日历统一 • 标的交叉验证 • 中位数填充 • 前向填充 • MAD法(推荐) • 百分位法 • 回归中性化 • 行业+市值 • 残差作为因子

数据准备与清洗,说白了就是「磨刀不误砍柴工」。我见过太多人急着跑模型,结果数据没处理好,白白浪费了时间。记住:好的数据,是因子有效性检验的基石

专注资料整理