数据质量检查:看不见的坑,最要命

做量化交易,说白了,策略逻辑再漂亮,输入的数据不对,结果就是废纸。我见过太多团队,花几个月调参、回测、优化,一上实盘就崩。为什么?数据质量不过关。

这一章,咱们聊聊四种最要命的数据问题:缺失值、异常值、生存偏差、前视偏差。每个我都吃过亏,有些亏现在还疼。

核心观点:数据质量问题不解决,回测结果再好看,也只是数字游戏。你赚的钱,可能是数据漏洞给你的幻觉。

数据质量检查知识体系 数据源 缺失值 异常值 生存偏差 前视偏差 填充/删除/插值 统计检测/截断 退市数据补齐 时间轴对齐 结论:数据清洗是策略可靠性的前提

一、缺失值:数据里的窟窿

举个简单的例子。你有一张日线行情表,某天某只股票的价格是 NaN。为什么会出现这种情况?可能是交易所数据断流,可能是行情商接口挂了,也可能是交易日历没对齐。

我个人习惯,收到任何数据源的第一件事就是跑一下缺失值检查。几行代码就能搞定的事,很多人偏偏跳过。

import pandas as pd
import numpy as np

# 加载日线数据
df = pd.read_csv('daily_data.csv', parse_dates=['date'], index_col='date')

# 检查每列的缺失比例
missing_ratio = df.isnull().sum() / len(df)
print(missing_ratio)

# 定位缺失的具体日期和股票
missing_dates = df[df['close'].isnull()].index
print(f"缺失收盘价的交易日: {len(missing_dates)} 天")

处理方式,我一般分三种情况:

  • 随机缺失(比如个别交易日没数据):用前值填充或用插值法补上。简单场景用 ffill,精致一点用线性插值。
  • 连续大面积缺失(比如某股票停牌一个月):直接删除这一段,不要造数据。你填进去的也不真实。
  • 交易日历不匹配(比如 A 股和港股假期不同):做对齐时慎重。我在项目中遇到过,用某个数据商的港股数据,竟然漏掉了圣诞节的交易日,导致因子计算全乱了。

提示:填充缺失值时,注意不要引入未来信息。比如用 ffill 是安全的,因为它只用过去的数据。用 bfill ?你要小心——那个未来值可能还没有发生。

二、异常值:谁在捣乱?

异常值,就是数据里那些离谱的数。比如某股票今天涨了 1000%,或者成交量突然多了几个量级。这种数据一看就不正常。

你想想看,如果策略是基于统计规律的,一个异常值可能把整个回归模型拉偏。我记得有一次回测年化收益做到了 45%,查了半天发现是因为某天的成交量数据多打了一个零。

识别方法,我常用两种:

  1. Z-score 法:把数据标准化,看哪些点离均值太远。一般 Z-score 值超过 3 的,就算异常。
  2. IQR 法:四分位距法。Q1 - 1.5*IQR 以下,Q3 + 1.5*IQR 以上,都标记为异常。
# Z-score 检测
from scipy import stats
z_scores = np.abs(stats.zscore(df['volume']))
outliers_z = df[z_scores > 3]
print(f"Z-score 检测到 {len(outliers_z)} 个异常成交量")

# IQR 检测
Q1 = df['volume'].quantile(0.25)
Q3 = df['volume'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['volume'] < Q1 - 1.5*IQR) | (df['volume'] > Q3 + 1.5*IQR)]
print(f"IQR 检测到 {len(outliers_iqr)} 个异常成交量")

处理方式上,我的建议是:

  • 金融数据的异常值,很多时候是真实事件(比如分红、送股、拆分)。不要去改它,而是要做复权处理。
  • 真正因为数据录入错误导致的异常值,直接删除或者用前后均值替换。
  • 遇到极端行情(比如 2015 年股灾),异常值反而是信号来源。要不要处理,看策略目的。

警告:不要对所有异常值一刀切。我曾经因为误删了分红日的价格数据,导致回测结果偏差了 3% 的年化收益。一定要先搞清楚异常产生的原因。

三、生存偏差:你以为的样本,其实不完整

这是个大坑。很多回测只用了「还活着」的股票——也就是到今天还在交易的股票。那些退市的、被收购的、跌没了的股票,都不在数据集里。

结果是什么?回测收益被高估了。因为你只看了活下来的幸存者,那些亏到退市的都不计数。

我记得有个公开的量化数据集,就是因为没处理生存偏差,被很多新手拿来跑回测,个个都觉得自己发现了圣杯。

怎么处理?

  • 必须使用包含退市股票的历史数据。很多数据商提供「退市数据包」,虽然贵,但值得买。
  • 回测时,把退市股票的最后一个交易日价格算入最终组合收益,然后股票池里把它删掉。
  • 特别提醒:退市股票的最后价格往往是零或很低,这对多空策略会带来真实的亏损模拟。
数据来源 是否含退市数据 生存偏差风险 我的建议
免费 Yahoo Finance 不全 高 慎用,需补退市数据
Wind/CSMAR 含有退市板块 低 可以用,但需确认覆盖完整
QuantConnect/Backtrader 部分 中等 仔细阅读其数据说明文档

提示:如果你用的是免费数据源,一个变通的方法是只回测“最近三年的活跃股票”。时间窗口短,生存偏差影响相对小。但长期回测,不能绕过这个问题。

四、前视偏差:用未来信息做决策

这可能是最隐蔽的坑。说白了,就是在回测的时候,你无意中用了「当时还看不到」的数据。

举个例子:你在计算某个因子时,用到了整个月的数据来做标准化。但事实上,在月初你根本不可能知道月底的数据。这就是前视偏差。

我遇到过最离谱的一次,是一个团队的策略年化收益 80%,夏普比 3.5。我一查代码,发现他们在「次日开盘价」计算中误用了「当日收盘价」。换句话说,他们的信号依赖于当天收盘后才可能知道的信息——这等于作弊。

如何避免?

  • 严格对齐时间轴:所有用于计算的输入数据,在时间上必须早于决策时刻。
  • 使用滞后算子:比如用 T-1 日的收盘价来计算 T 日的信号,而不是 T 日的收盘价。
  • 做一次批处理回测:把信号生成和交易执行拆分为两个阶段,中间引入至少一个 tick 的延迟。
# 错误示例:前视偏差
df['signal'] = np.where(df['close'].rolling(5).mean() > df['close'], 1, 0)
# 这里的 rolling(5) 用了当前及之前共5天的数据,但信号产生时刻,最后一天数据还没固定

# 正确做法:使用严格滞后的数据
df['signal'] = df['close'].shift(1).rolling(5).mean() > df['close'].shift(1)
# shift(1) 确保了信号是在下一个交易日才能用的

警告:前视偏差通常是最隐蔽,也是破坏力最大的数据问题。我的习惯是,写完回测代码后,再手动做一次「时间穿越检查」:假设今天是 2018 年 1 月 1 日,我能不能拿到这些数据?拿不到,就是前视偏差。

小结一下

数据质量检查,是策略失效排查的第一步,也是最重要的一步。缺失值、异常值、生存偏差、前视偏差,这四个问题,你至少要在数据清洗阶段全部过一遍。

我个人习惯,每次拿到新数据,先跑一个完整的数据质量报告,打印出下面这张表:

检查项目 检测方法 补救措施 优先级
缺失值 isnull() 逐列统计 向前填充 / 删除 / 插值 高
异常值 Z-score / IQR / 人工审查 判断原因后处理 高
生存偏差 检查退市股票是否在数据集内 补充退市数据 中
前视偏差 逐行检查 shift() 对齐 调整时间轴,加滞后 极高

数据干净了,策略检验才有意义。不然你就跟我当年一样,白高兴一场,然后熬夜查 bug。