第二章:数据准备与清洗

做量化策略,有一句话我特别认同:垃圾进,垃圾出。模型再牛,参数再优,数据不行,一切都是白搭。这一章,咱们就聊聊数据准备与清洗的那些事儿。

核心观点:数据清洗占整个因子开发工作量的60%以上,别嫌烦,这是基本功。

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

我个人习惯,数据源至少要备两个。一个主用,一个备用。为什么?因为数据源出问题,太常见了。

国内常用的数据源,我列个表给你看:

数据源 优点 缺点 我的建议
Wind 覆盖面广,机构认可度高 贵,个人用户不友好 机构首选
Tushare 免费,社区活跃 稳定性一般,有调用限制 个人研究够用
聚宽/JQData API友好,文档清晰 部分数据需要积分 量化入门推荐
AkShare 开源免费,数据种类多 接口变动频繁 适合爬虫能力强的

我在项目中遇到过,某次用Tushare拉日线数据,突然某天接口返回全为空。幸好我同时备了聚宽的数据,才没耽误回测。所以,永远不要只依赖一个数据源

小技巧:建议用Wind或聚宽作为主力数据源,Tushare或AkShare作为校验和补充。每次拉完数据,交叉验证一下关键字段,比如收盘价、成交量。

2.2 数据对齐:时间轴必须统一

多因子模型,说白了就是把不同来源的数据拼在一起。但问题来了——股票A有3000个交易日,股票B只有2800个,因子C的数据频率是日频,因子D是周频。怎么对齐?

我一般这么做:

  1. 确定基准时间轴:通常以交易日期为基准,剔除非交易日。
  2. 统一频率:所有数据都对齐到日频。周频数据,我习惯取每周最后一个交易日。
  3. 处理停牌:停牌期间的数据,用前值填充,或者直接设为NaN。

代码实现其实不复杂:

import pandas as pd

# 假设df1是股票日线,df2是因子数据
# 先确保日期列是datetime类型
df1['trade_date'] = pd.to_datetime(df1['trade_date'])
df2['date'] = pd.to_datetime(df2['date'])

# 合并时用outer join,保留所有日期
merged = pd.merge(df1, df2, left_on=['trade_date', 'stock_code'], 
                  right_on=['date', 'stock_code'], how='outer')

# 按股票和日期排序
merged.sort_values(['stock_code', 'trade_date'], inplace=True)

注意:千万不要用inner join!否则那些停牌日、非交易日的数据会被直接丢掉,回测结果会严重失真。我曾经因为这个坑,回测收益虚高了20%。

2.3 缺失值处理:填还是不填?

这是个经典问题。我的原则很简单:能填就填,不能填就扔

具体来说:

  • 缺失率 < 5%:用前向填充(ffill)或中位数填充。我个人偏好中位数,因为它不受极端值影响。
  • 缺失率 5%-20%:用行业均值填充。比如某只股票的市盈率缺失,就用同行业其他股票的市盈率中位数来填。
  • 缺失率 > 20%:直接删除这个因子或这只股票。别犹豫,数据质量太差,留着也是噪音。

举个例子:

# 前向填充
df['factor'] = df.groupby('stock_code')['factor'].ffill()

# 行业均值填充
df['factor'] = df.groupby('industry')['factor'].transform(
    lambda x: x.fillna(x.median())
)

嗯,这里要注意:千万不要用全局均值填充。为什么?因为不同行业的因子分布差异很大,全局均值会抹平行业特征,导致因子失效。我刚开始做的时候犯过这个错,后来发现因子IC值怎么调都不对,查了半天才发现是填充方式的问题。

2.4 异常值检测与处理:别让一颗老鼠屎坏了一锅粥

异常值,说白了就是那些离谱的数据。比如某股票市盈率突然变成10000倍,或者成交量突然放大100倍。这些数据如果不处理,模型会被带偏。

我常用的方法有三种:

方法 原理 适用场景
3σ原则 超过均值±3倍标准差视为异常 数据近似正态分布
IQR方法 超过Q1-1.5IQR或Q3+1.5IQR视为异常 数据分布偏态
MAD方法 基于中位数绝对偏差,更稳健 存在较多极端值

我个人最常用的是MAD方法,因为它对极端值不敏感。代码实现:

import numpy as np

def mad_detection(series, threshold=3.5):
    median = np.median(series)
    mad = np.median(np.abs(series - median))
    modified_z_scores = 0.6745 * (series - median) / mad
    return np.abs(modified_z_scores) > threshold

# 应用
df['is_outlier'] = df.groupby('trade_date')['factor'].transform(
    lambda x: mad_detection(x)
)
df.loc[df['is_outlier'], 'factor'] = np.nan  # 设为缺失,后续填充

避坑指南:我曾经直接用3σ原则处理所有因子,结果发现有些因子的分布本身就是尖峰厚尾的,3σ会误删很多正常数据。后来我改用MAD方法,效果好了很多。记住:没有万能的方法,要根据数据分布特点来选择

2.5 数据标准化:让因子站在同一起跑线

不同因子的量纲差异很大。比如市盈率可能是几倍到几百倍,换手率可能是0.1%到10%。如果不标准化,模型会天然偏向数值大的因子。

常用的标准化方法:

  • Z-score标准化:(x - mean) / std。适用于数据近似正态分布。
  • Min-Max标准化:(x - min) / (max - min)。将数据映射到[0,1]区间。
  • Rank标准化:将数据排序后转为百分位数。对异常值最稳健。

我个人的经验是:做截面标准化,不要做时间序列标准化。什么意思?就是每个交易日,对所有股票的因子值进行标准化,而不是对某只股票的历史数据做标准化。因为截面标准化能保留横截面上的相对强弱信息,这才是多因子模型需要的。

# 截面Z-score标准化
df['factor_zscore'] = df.groupby('trade_date')['factor'].transform(
    lambda x: (x - x.mean()) / x.std()
)

# 截面Rank标准化
df['factor_rank'] = df.groupby('trade_date')['factor'].transform(
    lambda x: x.rank(pct=True)
)

重要提醒:标准化一定要在训练集上计算参数,然后应用到测试集。千万不要在整个数据集上做标准化,否则会引入未来信息,导致回测结果虚高。这个坑我踩过,回测收益漂亮得不行,实盘直接打脸。

本章知识体系

下面这张图,帮你梳理数据准备与清洗的完整流程:

数据准备与清洗流程 数据源选择 Wind / Tushare / 聚宽 数据对齐 统一时间轴 / 频率 缺失值处理 填充 / 删除 异常值检测 3σ / IQR / MAD 异常值处理 截尾 / 替换 / 删除 数据标准化 Z-score / Rank 输出:干净、对齐、标准化的多因子数据集 可用于后续因子检验、模型训练与回测

数据准备与清洗,说白了就是给模型喂好料。这一步做扎实了,后面的因子检验和模型训练才能事半功倍。别嫌麻烦,我见过太多人在这上面偷懒,结果回测和实盘差距巨大,最后还得回头重新清洗数据。

记住一句话:数据质量决定了策略收益的上限,模型只是逼近这个上限的工具

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