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

做量化的人都知道一句话:垃圾进,垃圾出。模型再牛,数据不行,一切都是白搭。

我个人习惯把数据准备看作做菜前的备料。你想想看,食材不新鲜、没洗干净,再好的厨艺也白费。多因子模型也是这个道理。今天我们就聊聊数据准备与清洗的那些事。

核心观点:数据清洗占整个因子开发工作量的60%以上。别急着跑模型,先把数据搞干净。

3.1 数据源选择:Wind、Tushare、聚宽

做多因子模型,第一步就是选数据源。国内主流的有三个:Wind、Tushare、聚宽。我三个都用过,说说我的感受。

数据源 优点 缺点 适合场景
Wind 数据全、更新快、机构认可度高 贵(一年几万起)、需要Windows环境 机构、专业团队
Tushare 免费、社区活跃、接口灵活 稳定性一般、有调用次数限制 个人研究、小团队
聚宽 本地+云端、因子库丰富、回测方便 部分数据需要积分、定制化有限 量化入门、策略开发

我在项目中遇到过一个问题:用Tushare拉数据,某天突然发现某只股票的复权因子算错了。排查了半天,原来是接口返回的数据有延迟。从那以后,我养成了一个习惯——多源交叉验证。至少用两个数据源对比一下,心里才踏实。

我的建议:如果预算允许,Wind做主力数据源,Tushare做辅助验证。个人研究的话,聚宽是个不错的选择,省去了很多数据维护的麻烦。

3.2 数据对齐:时间对齐与截面对齐

数据对齐是个容易被忽视的坑。你想想看,不同股票的交易日期不一样,不同因子的更新频率也不一样。不对齐就往下跑,结果肯定乱套。

数据对齐分两种:

  • 时间对齐:把不同时间频率的数据统一到同一个时间轴上。比如日频因子和月频因子,得对齐到同一个交易日。
  • 截面对齐:在同一时间点上,确保所有股票都有数据。有些股票停牌了、退市了,得处理掉。

我一般用pandas的reindexmerge来做对齐。举个例子:

import pandas as pd

# 假设我们有日频的收益率数据和周频的估值数据
daily_returns = pd.read_csv('daily_returns.csv', index_col=0, parse_dates=True)
weekly_pe = pd.read_csv('weekly_pe.csv', index_col=0, parse_dates=True)

# 对齐到日频,周频数据向前填充
aligned_pe = weekly_pe.reindex(daily_returns.index, method='ffill')

# 合并
aligned_data = pd.merge(daily_returns, aligned_pe, left_index=True, right_index=True)

注意:向前填充(ffill)要谨慎。如果某只股票停牌太久,用前几周的数据填充,可能会引入偏差。我曾经因为这个原因,在回测中得到了一个看起来很美的策略,实盘却亏得一塌糊涂。

3.3 去极值:别让异常值毁了你的模型

极值问题在金融数据里太常见了。某只股票突然涨停,市盈率飙到1000倍;或者某天成交量异常放大,换手率冲到50%。这些极端值如果不处理,会严重影响因子的有效性。

常用的去极值方法有三种:

  1. MAD法(中位数绝对偏差):稳健性最好,适合大多数场景。
  2. 百分位法:简单粗暴,直接砍掉上下1%或5%的数据。
  3. 标准差法:假设数据服从正态分布,超过3倍标准差就截断。

我个人习惯用MAD法。为什么?因为它对异常值不敏感。你想想看,如果数据本身就有很多异常值,用标准差法反而会被带偏。

def winsorize_mad(series, n=5):
    """
    基于MAD的去极值处理
    n: 阈值倍数,默认5倍MAD
    """
    median = series.median()
    mad = (series - median).abs().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.4 缺失值处理:填还是不填?

缺失值处理是个老生常谈的问题。但说实话,没有万能的方法。关键看你的数据缺失是什么原因造成的。

常见的缺失值处理方法:

方法 适用场景 缺点
删除缺失值 缺失比例很小(<5%) 可能丢失信息
向前/向后填充 时间序列数据,缺失时间短 引入滞后偏差
均值/中位数填充 截面数据,缺失随机 降低方差
模型预测填充 缺失比例较高 计算量大,可能过拟合

我个人的经验是:先看缺失原因,再选方法。如果是因为停牌导致的缺失,用向前填充比较合理。如果是因为数据源没覆盖到,那可能得考虑删除或者用模型预测。

# 一个简单的填充策略
def fill_missing(df, method='ffill', limit=5):
    """
    缺失值填充
    method: ffill(向前填充), bfill(向后填充), median(中位数填充)
    limit: 最大填充天数
    """
    if method == 'ffill':
        return df.fillna(method='ffill', limit=limit)
    elif method == 'bfill':
        return df.fillna(method='bfill', limit=limit)
    elif method == 'median':
        return df.fillna(df.median())
    else:
        raise ValueError("不支持的填充方法")

注意:千万别一股脑全用均值填充。我见过有人把所有缺失值都填成均值,结果因子分布变得很奇怪,模型训练出来完全不能用。缺失值处理要因变量而异,因场景而异。

3.5 标准化:让因子站在同一起跑线上

标准化是因子处理最后一步,也是最关键的一步。不同因子的量纲不一样,市盈率可能是几十倍,换手率可能是百分之几。不标准化,模型就会偏向那些数值大的因子。

常用的标准化方法:

  • Z-score标准化:减去均值,除以标准差。假设数据服从正态分布。
  • Min-Max标准化:缩放到[0,1]区间。对异常值敏感。
  • Rank标准化:把数值转换成排名,再映射到[0,1]。稳健性最好。

我个人最喜欢Rank标准化。为什么?因为它不受极端值影响。你想想看,如果某只股票的市盈率是1000倍,Z-score标准化后它还是异常大,但Rank标准化后它只是排名第一而已。

def rank_standardize(series):
    """
    Rank标准化:将数值转换为排名,再映射到[0,1]
    """
    rank = series.rank()
    return (rank - 1) / (len(rank) - 1)

# 使用示例
factor_data['pe_rank'] = rank_standardize(factor_data['pe_ratio'])

我的经验:标准化之后,最好检查一下因子的分布。如果某个因子标准化后还是偏态严重,那可能得考虑做Box-Cox变换或者对数变换。我在一个项目中就遇到过这种情况,做了对数变换后,因子效果明显提升。

3.6 本章知识体系

说了这么多,我们来梳理一下数据准备与清洗的完整流程。下面这张图可以帮你快速理解:

数据准备与清洗流程 数据源选择 数据对齐 去极值 缺失值处理 标准化 输出可用因子 Wind / Tushare / 聚宽 时间对齐 / 截面对齐 MAD / 百分位 / 标准差 删除 / 填充 / 模型预测 Z-score / Min-Max / Rank 因子矩阵 / 回测输入 💡 核心原则:先看数据质量,再选处理方法 多源交叉验证,避免单一数据源带来的偏差

数据准备与清洗,说白了就是为模型打好地基。地基不稳,楼盖得再高也得塌。我见过太多人急着跑模型、调参数,结果数据都没搞干净,最后白忙活一场。

嗯,这一章的内容就到这里。记住:数据清洗不是一次性的工作,而是贯穿整个因子开发过程的持续任务。每次新增因子、每次数据更新,都要重新走一遍这个流程。

蓝海数据掘金营,专注资料整理