第三章 数据获取与清洗:金融数据源介绍、Pandas数据处理、缺失值处理、极端值处理、行业市值中性化

做量化研究,有一句老话叫「垃圾进,垃圾出」。你因子逻辑再漂亮,回测框架再牛,数据一塌糊涂,结果就是自欺欺人。我见过太多人花几个月挖因子,最后发现是数据没洗干净——那种感觉,比亏钱还难受。

这一章,咱们就聊聊数据那些事。从哪拿数据,拿到后怎么处理,遇到坑怎么填。嗯,都是实战中摸爬滚打出来的经验。

3.1 金融数据源:你的弹药库

先说说数据源。国内常用的就那么几个,我按个人偏好排个序:

数据源 特点 适合场景 我的评价
Wind 最全,但贵 机构实盘 行业标准,没它不行
聚宽/JQData API友好,Python原生 回测研究 个人最爱,上手快
Tushare 免费,社区活跃 学习入门 良心项目,但稳定性一般
RiceQuant 云端策略 快速验证 适合不想搭环境的人

我个人习惯用聚宽的数据接口。为什么?因为它直接返回DataFrame,省去了我写解析代码的时间。你想想看,每天处理几百个因子,如果每个数据源都要写一套解析逻辑,那得累死。

小提示: 如果你刚开始做因子研究,建议先用聚宽或Tushare免费版。等因子库稳定了,再考虑上Wind。别一上来就花几万买数据,万一因子不work呢?

3.2 Pandas数据处理:你的瑞士军刀

拿到数据后,第一件事是什么?不是跑因子,是看数据长什么样。我每次拿到新数据,都会先跑这几行代码:

import pandas as pd
import numpy as np

# 假设df是日频行情数据
print(df.shape)          # 看看有多大
print(df.head())         # 瞄一眼前几行
print(df.info())         # 检查数据类型和缺失情况
print(df.describe())     # 看分布统计

这一步看似简单,但能帮你发现很多问题。比如某列全是NaN,或者日期格式不对,或者价格出现了负数——嗯,这些我都遇到过。

接下来是数据对齐。因子研究里,不同数据源的频率可能不一样。有的日频,有的月频,有的甚至是不规则频率。我的做法是统一用交易日历做索引:

# 对齐到交易日历
trade_cal = get_trade_days(start_date, end_date)
df = df.reindex(trade_cal)

为什么要这么做?因为金融数据天然有缺失——周末、节假日都没数据。如果你不显式对齐,pandas会给你搞出很多奇怪的索引对齐问题。我曾经因为这个bug排查了整整两天,最后发现是索引没对齐。

3.3 缺失值处理:别让NaN毁了你的因子

缺失值,说白了就是数据没拿到。原因很多:停牌、新股上市、数据源抽风……

处理缺失值,我一般分三步走:

  1. 先诊断:看看缺失比例。超过50%的列,直接扔掉。
  2. 再分类:是随机缺失还是系统性缺失?停牌导致的缺失是系统性的,需要特殊处理。
  3. 最后填充:常用的方法有向前填充、插值、用行业均值填充。

代码示例:

# 第一步:检查缺失比例
missing_ratio = df.isnull().mean()
cols_to_drop = missing_ratio[missing_ratio > 0.5].index
df.drop(columns=cols_to_drop, inplace=True)

# 第二步:向前填充(适合停牌数据)
df.fillna(method='ffill', inplace=True)

# 第三步:剩余缺失用行业均值填充
df['factor'] = df.groupby('industry')['factor'].transform(
    lambda x: x.fillna(x.mean())
)
注意: 千万别用全局均值填充!不同行业的因子分布差异很大。我曾经见过有人用全市场均值填充市盈率,结果金融股的PE被拉低了一大截,因子效果直接崩了。

3.4 极端值处理:别让异常值带偏你的模型

极端值,就是那些离谱的数据。比如某天某只股票涨了1000%,或者市盈率突然变成负数——这些基本都是数据错误,或者是一些特殊事件(比如新股上市首日)。

处理极端值,我常用的方法有三种:

方法 原理 适用场景
MAD法 基于中位数绝对偏差 因子分布不对称时
百分位法 截断上下1%/5% 快速清洗,简单粗暴
标准差法 超过3倍标准差截断 正态分布假设下

我个人偏好MAD法。为什么?因为它对异常值更鲁棒。标准差法受极端值影响太大——你想想看,如果数据里有一个极端大的值,标准差会被拉得很大,结果就是正常值反而被截断了。

代码实现:

def winsorize_mad(series, n=5):
    """基于MAD的缩尾处理"""
    median = series.median()
    mad = (series - median).abs().median()
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

# 应用
df['factor_winsorized'] = df.groupby('date')['factor'].transform(winsorize_mad)

注意这里我按日期分组处理。为什么?因为不同时间点的因子分布可能不同。牛市和熊市,因子的波动范围差远了。

3.5 行业市值中性化:让因子「纯净」起来

这一步,是因子研究的核心环节。说白了,就是剔除行业和市值对因子的影响。

为什么要做中性化?举个例子:你发现小市值股票表现好,但这可能只是因为小市值因子在起作用,而不是你挖的那个因子本身有效。中性化就是要把这种「搭便车」的影响去掉。

做法很简单:用因子值对行业虚拟变量和市值做回归,取残差作为新的因子值。

import statsmodels.api as sm

def neutralize_factor(df, factor_col, industry_col, market_cap_col):
    """行业市值中性化"""
    # 创建行业虚拟变量
    industry_dummies = pd.get_dummies(df[industry_col], prefix='ind')
    
    # 构建回归模型
    X = pd.concat([industry_dummies, df[[market_cap_col]]], axis=1)
    X = sm.add_constant(X)
    y = df[factor_col]
    
    # 回归取残差
    model = sm.OLS(y, X).fit()
    df['factor_neutral'] = model.resid
    
    return df
关键点: 中性化一定要在截面(同一时间点)上做。跨时间的中性化没有意义,因为行业和市值的影响是随时间变化的。

我曾经犯过一个错误:在全体数据上做了一次中性化,结果因子效果看起来特别好。后来才发现,因为不同时期的市值分布不同,这种做法相当于「偷看了未来数据」。嗯,那次的教训挺深刻的。

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

数据获取与清洗流程 金融数据源 Pandas数据处理 缺失值处理 极端值处理 行业市值中性化 干净可用的因子数据 每一步都直接影响因子最终的有效性,缺一不可

数据清洗这件事,说难不难,说简单也不简单。难的是你永远不知道下一个坑在哪。我做了这么多年,每次拿到新数据还是会小心翼翼。记住一句话:对数据保持敬畏,它就不会坑你。

专注资料整理