数据清洗的坑:异常值、幸存者偏差、前视偏差与对齐错误

做量化交易的朋友,尤其是搞另类数据的,一定听过这句话:「垃圾进,垃圾出。」

数据清洗,说白了就是给原始数据「排雷」。我见过太多团队,模型跑得飞起,回测曲线漂亮得不像话,结果一上实盘就崩。为什么?数据里有鬼。

今天咱们就聊聊四个最常见的坑:异常值、幸存者偏差、前视偏差、数据对齐错误。每个我都踩过,每个都让我亏过钱。

核心观点:数据清洗不是体力活,是决定策略生死的关键步骤。80%的量化失败,根源都在数据质量。

1. 异常值处理:别把信号当噪声

异常值,就是那些「离谱」的数据点。比如某只股票一天涨了1000%,或者某个月成交量突然变成平时的100倍。

很多人第一反应是:删掉。或者用均值填充。嗯,这其实很危险。

为什么?

  • 有些异常值是真实的极端事件(比如财报暴雷、政策突变)
  • 有些异常值是数据采集错误(比如传感器故障、API返回乱码)
  • 还有些是数据源本身的问题(比如复权计算错误)

我在项目中遇到过一件事:某次回测一个事件驱动策略,收益高得离谱。查了半天,发现是某只股票在除权日的数据没处理好,价格瞬间「暴跌」90%,策略自动买入,第二天又涨回来。这哪是策略赚钱?这是数据在骗人。

我的建议:先分类,再处理。

  • 明显的数据错误(如负数价格、日期乱码)→ 直接剔除
  • 极端但合理的值(如涨跌幅超过20%)→ 用Winsorize截尾处理
  • 不确定的 → 标记出来,单独分析
# 一个简单的异常值处理示例
import numpy as np

def winsorize_series(series, limits=[0.01, 0.01]):
    """
    对序列进行截尾处理
    limits[0]:下尾截尾比例
    limits[1]:上尾截尾比例
    """
    lower = series.quantile(limits[0])
    upper = series.quantile(1 - limits[1])
    return series.clip(lower, upper)

# 我个人习惯先看分布,再决定截尾比例
# 比如用IQR方法先标记异常值,再人工复核

2. 幸存者偏差:你看到的都是活下来的

幸存者偏差,是量化回测里最隐蔽的杀手。

什么意思?你拿今天还在交易的股票做回测,那些退市的、被收购的、破产的,全被你忽略了。结果就是:回测收益被高估,风险被低估。

我记得有一次帮朋友复盘一个多因子策略。他用的数据是「当前沪深300成分股」。我说你回测区间是2015-2020,那时候的成分股和现在一样吗?他愣住了。

2015年的沪深300,到2020年至少有30%的股票被替换掉了。那些被踢出去的,很多都是表现差的。你只拿活下来的做回测,能不赚钱吗?

避坑指南:我曾经犯过这个错,后来学乖了。

  • 必须使用「历史时点」的股票池,不能用当前数据回溯
  • 退市、ST、暂停上市的股票,都要保留在回测中
  • 做指数增强策略时,要使用指数当时的成分股列表

你想想看,如果只统计活到今天的公司,那「买入并持有」策略的回测收益率会高得离谱。因为那些倒闭的公司,数据根本不会出现在你的数据库里。

3. 前视偏差:未来信息穿越了

前视偏差,也叫未来函数。就是你在回测时,不小心用到了「未来才知道」的信息。

举个最简单的例子:你用当天的收盘价计算指标,然后判断当天是否买入。但收盘价是在收盘后才确定的,你不可能在盘中就知道收盘价。

更隐蔽的情况呢?

  • 用财报数据时,没考虑财报发布日期(你以为Q1数据在4月底可用,其实可能5月才披露)
  • 用新闻情绪数据时,没考虑新闻发布时间(你以为开盘前就能用,其实新闻是盘中才发的)
  • 用因子数据时,用了未来信息做标准化(比如用全样本的均值和标准差)

关键原则:回测时,任何数据都必须使用「当时能获取到的」版本。做不到这一点,回测就是自欺欺人。

# 前视偏差的典型例子
# 错误做法:用未来数据做标准化
def wrong_standardize(df):
    mean = df['factor'].mean()  # 用了全样本均值
    std = df['factor'].std()    # 用了全样本标准差
    return (df['factor'] - mean) / std

# 正确做法:用滚动窗口
def correct_standardize(df, window=252):
    rolling_mean = df['factor'].rolling(window).mean()
    rolling_std = df['factor'].rolling(window).std()
    return (df['factor'] - rolling_mean) / rolling_std

我个人习惯在回测框架里加一个「时间戳检查」:每个数据点都带上它的「可用时间」,回测引擎只允许使用可用时间之前的数据。这样能杜绝99%的前视偏差。

4. 数据对齐错误:时间不同步

这个坑,说实话,比前三个都难发现。

数据对齐错误,就是不同数据源的时间戳没对上。比如股票数据用的是北京时间,新闻数据用的是UTC时间,差8个小时。或者日线数据有的用收盘时间,有的用开盘时间。

我在做另类数据时遇到过:卫星图像数据的时间戳是UTC,A股交易数据是北京时间。我一开始没注意,结果回测里「提前」看到了下午的卫星图,用来预测上午的行情。这能不赚钱吗?

我的检查清单:

  1. 统一时区:全部转成同一个时区(建议用UTC+0)
  2. 统一频率:日线、分钟线、Tick数据,对齐到同一时间粒度
  3. 检查边界:比如周五的夜盘数据,算哪一天?
  4. 做交叉验证:用两个独立数据源对比,看时间戳是否一致
数据源 原始时间戳 对齐后时间戳 常见问题
A股日线 2024-01-15 15:00:00 CST 2024-01-15 07:00:00 UTC 注意夏令时
新闻API 2024-01-15 06:30:00 UTC 2024-01-15 06:30:00 UTC 通常没问题
卫星数据 2024-01-15 06:45:00 Z 2024-01-15 06:45:00 UTC Z就是UTC

数据对齐错误,最坑的地方在于:它不会让你的回测直接崩溃,而是让收益「看起来」更好。因为未来信息偷偷混进来了。你查代码查三天都查不出来。

知识体系总览

下面这张图,是我自己总结的数据清洗核心逻辑。四个坑,每个都有对应的处理思路。

数据清洗四大陷阱与应对策略 原始数据 异常值 幸存者偏差 前视偏差 数据对齐错误 应对策略 分类处理 Winsorize截尾 应对策略 历史时点股票池 保留退市数据 应对策略 时间戳检查 滚动窗口标准化 应对策略 统一时区 交叉验证 核心原则:回测必须模拟「当时能获取到的」数据

嗯,这四个坑,每一个都能让你的回测结果失真。我个人的经验是:数据清洗花的时间,至少占整个项目时间的40%。别嫌多,这钱花得值。

最后提醒一句:不要相信任何未经清洗的回测结果。哪怕数据源再权威,也要自己过一遍。我在这个上面吃过太多亏了。


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