4. 数据准备与清洗:获取行情数据、财务数据、因子数据,处理缺失值、异常值、幸存者偏差

做量化的人都知道一句话:垃圾进,垃圾出。模型再漂亮,数据一塌糊涂,结果就是自欺欺人。我见过太多人花几个月调参数,最后发现是数据没洗干净——那种感觉,就像你跑完马拉松才发现鞋里全是沙子。

这一章,咱们就聊聊数据准备这件事。说白了,就是怎么把原始数据变成能喂给模型吃的干净食材。

4.1 数据来源:行情、财务、因子

先说说数据从哪来。我个人习惯把数据分成三大类:

  • 行情数据:开盘价、收盘价、最高价、最低价、成交量、成交额。这是最基础的,也是频率最高的。
  • 财务数据:营收、净利润、ROE、资产负债率等。季度更新,频率低但信息量大。
  • 因子数据:基于行情和财务加工出来的衍生指标,比如动量因子、价值因子、质量因子。

你想想看,这三类数据就像盖房子的砖、水泥和钢筋。缺了哪样,房子都不稳。

核心原则:数据源要稳定、可追溯。我建议至少准备两个独立的数据源做交叉验证。曾经有一次,我发现某个数据源的复权价格算错了,要不是有另一个源做对比,那整个回测就废了。

4.2 数据清洗:缺失值处理

缺失值,是量化里最烦人的问题之一。为什么会有缺失?原因很多:停牌、数据源漏了、新股上市第一天没有历史数据……

处理方式,我一般分三步走:

  1. 识别缺失模式:是随机缺失,还是系统性缺失?比如ST股经常缺失财务数据,这就是系统性的。
  2. 选择填充方法
    • 前向填充(ffill):用上一个交易日的数据填充。适合行情数据。
    • 后向填充(bfill):用下一个交易日的数据填充。慎用,容易引入未来信息。
    • 插值法:线性插值、多项式插值。适合财务数据这种低频序列。
    • 行业均值填充:用同行业其他公司的均值替代。适合因子数据。
  3. 标记缺失:我习惯在数据里加一列缺失标记,方便后续分析。

我的经验:千万别一股脑全填充。我曾经在回测里用后向填充处理停牌数据,结果策略表现好得离谱——后来才发现,停牌复牌后的跳空被当成了策略收益。嗯,那叫一个尴尬。

4.3 异常值处理

异常值,就是那些明显偏离正常范围的数据点。比如某只股票一天涨了1000%,或者市盈率突然变成负数的一万倍。

为什么会这样?数据录入错误、除权除息没处理好、极端市场事件……原因很多。

我常用的方法:

  • MAD法:中位数绝对偏差。比标准差更稳健,不受极端值影响。
  • 分位数截断:比如把上下1%的数据截掉。简单粗暴,但有效。
  • 业务逻辑判断:比如市盈率不可能为负(除非亏损),涨跌幅不可能超过100%(A股涨停板限制)。

注意:异常值不一定是错误。比如2015年股灾时,很多股票连续跌停,那确实是真实数据。所以处理异常值之前,先问问自己:这个异常是数据问题,还是市场真实情况?

4.4 幸存者偏差:最隐蔽的坑

幸存者偏差,说白了就是你只看到了活下来的,忽略了死掉的

举个例子:你回测一个选股策略,用的数据是今天还在交易的股票。但那些退市的、被ST的、跌成渣的股票呢?它们的数据被你自动过滤掉了。结果就是,你的策略看起来特别牛,因为只选了「幸存者」。

我刚开始做量化时,就踩过这个坑。回测年化收益30%,实盘一跑直接亏成狗。后来才发现,数据里全是活到现在的股票,那些退市的垃圾股根本没参与回测。

怎么避免?

  • 使用全量数据:包括已经退市的、被并购的、暂停上市的股票。
  • 动态回测:在回测的每一天,只使用当时真实存在的股票。
  • 加入退市标记:在数据里标记哪些股票后来退市了,方便做敏感性分析。

一句话总结:回测时,你只能知道过去的信息。别让未来的信息偷偷溜进你的数据里。

4.5 数据清洗流程框架

下面这张图,是我自己总结的数据清洗流程。每次做新项目,我都会按这个框架走一遍。

数据清洗流程框架 原始数据 缺失值处理 前向填充 | 后向填充 | 插值法 | 行业均值 异常值处理 MAD法 | 分位数截断 | 业务逻辑判断 幸存者偏差处理 全量数据 | 动态回测 | 退市标记 干净数据

4.6 代码示例:数据清洗实战

光说不练假把式。下面这段代码,是我在项目中常用的数据清洗流程。你直接拿去用,改改参数就行。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('stock_data.csv', index_col='date', parse_dates=True)

# 1. 缺失值处理
# 前向填充(行情数据)
df['close'] = df['close'].fillna(method='ffill')

# 行业均值填充(因子数据)
df['pe_ratio'] = df.groupby('industry')['pe_ratio'].transform(
    lambda x: x.fillna(x.mean())
)

# 2. 异常值处理
# MAD法
def mad_outlier(series, threshold=3):
    median = series.median()
    mad = np.abs(series - median).median()
    return np.abs(series - median) / mad > threshold

df = df[~mad_outlier(df['return_1d'])]

# 3. 幸存者偏差处理
# 确保包含退市股票
df = df[df['is_alive'] == True]  # 回测时只取当时存活的股票

print('数据清洗完成,剩余记录数:', len(df))

小技巧:我习惯把清洗逻辑封装成一个函数,每次跑新数据直接调用。这样既省时间,又不容易出错。

4.7 数据质量检查清单

最后,分享一个我每次做数据清洗都会检查的清单。你可以打印出来贴在工位上。

检查项 说明 检查方法
缺失率 每个字段的缺失比例 df.isnull().mean()
异常值 是否存在极端值 描述性统计 + 箱线图
幸存者偏差 是否包含退市股票 检查股票数量是否随时间递减
未来信息 是否使用了未来数据 检查填充方法是否引入了未来值
数据一致性 不同数据源是否一致 交叉验证

数据准备这件事,看起来枯燥,但它是整个量化策略的基石。我见过太多人在这上面翻车,包括我自己。所以,别嫌麻烦,把数据洗干净了再上模型。

记住一句话:数据干净了,策略就成功了一半

专注资料整理