数据获取与清洗:构建稳健多因子模型的基石

做多因子模型,有个残酷的现实:你花在数据清洗上的时间,往往比建模本身还多。我刚开始做量化那会儿,总觉得策略牛逼就行,结果被数据坑过好几次。后来学乖了——数据质量不过关,再漂亮的模型也是空中楼阁。

今天咱们就聊聊数据获取与清洗这四板斧:数据源选择、缺失值处理、去极值、标准化。每一步都有坑,我一个个说。

数据获取与清洗 数据源选择 Wind / Tushare / 聚宽 缺失值处理 删除 / 填充 / 插值 去极值 MAD / 百分位 / 3σ 标准化 Z-score / Min-Max / Rank 目标:干净、一致、可复用的因子数据

一、数据源选择:别在源头就输了

数据源的选择,直接决定了你后面所有工作的质量。我个人习惯把数据源分成三类:

类型 代表 优点 缺点
商业数据库 Wind、Bloomberg 数据全、质量高、有售后 贵,个人用户不友好
开源平台 Tushare、AKShare 免费、社区活跃 偶有数据缺失、接口不稳定
量化平台 聚宽、米筐 自带清洗、回测方便 数据范围受限
我的建议:刚开始做研究,用Tushare或AKShare就够了。等策略跑通了,再上商业数据。别一上来就花大价钱买Wind,万一策略不行呢?

这里有个关键点:不同数据源对同一只股票的同一指标,可能给出不同值。我在项目中遇到过,用Wind和Tushare分别取某只股票的市盈率,差了0.3。原因可能是复权方式不同,也可能是数据源更新频率不一样。所以,同一个因子,全程只用同一个数据源,别混着用。

二、缺失值处理:别让NaN毁了你的模型

拿到数据后第一件事——检查缺失值。你想想看,如果某个因子在30%的股票上都是NaN,那这个因子基本废了。

处理缺失值,我一般按这个优先级来:

  1. 删除法:缺失比例超过50%的因子,直接扔掉。别心疼。
  2. 填充法:缺失比例在10%-50%之间,用行业均值填充。为什么用行业均值?因为同行业的公司财务特征更接近。
  3. 插值法:时间序列数据,用前后值线性插值。比如某天停牌导致数据缺失,用前后交易日的数据补上。
避坑指南:千万不要用全局均值填充!我曾经犯过这个错——把所有股票的缺失值都填上整体均值,结果因子区分度大幅下降。因为不同行业的指标差异很大,用全局均值相当于把行业特征抹平了。

代码实现其实很简单:

import pandas as pd
import numpy as np

# 假设df是因子数据,index是股票,columns是时间
# 1. 删除缺失率超过50%的因子
df = df.dropna(thresh=len(df)*0.5, axis=1)

# 2. 用行业均值填充
# 假设有行业标签列 'industry'
df = df.groupby('industry').transform(lambda x: x.fillna(x.mean()))

# 3. 时间序列插值
df = df.interpolate(method='linear', axis=0)

三、去极值:别让异常值带偏了

因子数据里经常有极端值。比如某只股票因为重组,市盈率突然飙到1000倍。这种值如果不处理,会严重扭曲后续的标准化和模型结果。

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

方法 原理 适用场景
MAD法 基于中位数绝对偏差 数据分布偏态严重时
百分位法 截断上下1%/5% 简单粗暴,快速有效
3σ法 超过均值±3倍标准差 数据近似正态分布时

我个人最常用的是MAD法。为什么?因为中位数对异常值不敏感,比均值稳健得多。你想想看,如果数据里有几个极端值,均值会被拉偏,但中位数基本不受影响。

注意:去极值不是删除数据,而是把极端值替换成边界值。比如用3σ法,超过均值+3σ的值,直接设为均值+3σ。这叫"截断"或"缩尾",不是"剔除"。

代码示例:

def winsorize_mad(series, n=5):
    """MAD法去极值"""
    median = series.median()
    mad = np.abs(series - median).median()
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

# 应用
df_clean = df.apply(winsorize_mad, axis=0)

四、标准化:让不同因子站在同一起跑线

做完去极值,下一步就是标准化。为什么要标准化?因为不同因子的量纲不一样。比如市盈率可能是几十倍,而换手率是百分之几。如果不标准化,模型会天然偏向数值大的因子。

常用的标准化方法:

  • Z-score标准化:(x - mean) / std。假设数据服从正态分布,标准化后均值为0,标准差为1。
  • Min-Max标准化:(x - min) / (max - min)。把数据压缩到[0,1]区间,但容易受极端值影响。
  • Rank标准化:把数值转换成排名,再映射到[0,1]。对异常值最鲁棒,但丢失了数值间的距离信息。
我的经验:做截面多因子模型时,我倾向于用Z-score标准化。因为它在保留数值分布信息的同时,还能让不同因子可比。但要注意——必须在每个时间截面上独立做标准化,不能跨时间做。否则会引入未来信息。

代码实现:

def standardize_zscore(df):
    """截面Z-score标准化"""
    mean = df.mean(axis=0)
    std = df.std(axis=0)
    return (df - mean) / std

# 每个时间点独立标准化
df_standardized = df.groupby(level='date').apply(standardize_zscore)

五、完整流程:从原始数据到可用因子

好了,把上面四步串起来,就是一个完整的数据清洗流水线:

def clean_factor_pipeline(df_raw):
    """
    完整的数据清洗流程
    输入:原始因子数据
    输出:清洗后的因子数据
    """
    # 1. 删除缺失过多的因子
    df = df_raw.dropna(thresh=len(df_raw)*0.5, axis=1)
    
    # 2. 行业均值填充缺失值
    df = df.groupby('industry').transform(lambda x: x.fillna(x.mean()))
    
    # 3. MAD法去极值
    df = df.apply(winsorize_mad, axis=0)
    
    # 4. Z-score标准化
    df = df.groupby(level='date').apply(standardize_zscore)
    
    return df

嗯,到这里,数据清洗的核心内容就讲完了。记住一句话:数据清洗不是锦上添花,而是生死攸关。我见过太多人花几个月建模型,结果因为数据没洗干净,回测曲线漂亮得不行,实盘一跑就崩。别走那条弯路。

公众号:蓝海资料掘金营,微信deep3321