3. 数据质量初探:缺失值检测、异常值识别、重复数据处理

做市商这行,数据就是命根子。我见过太多团队,模型建得花里胡哨,结果一跑就崩——查到最后,全是数据质量的问题。说白了,你喂进去的是垃圾,出来的只能是更精致的垃圾。

这一节,咱们就聊聊数据清洗的第一步:缺失值、异常值、重复数据。这三个东西,是外汇数据里最常见的“坑”。

核心观点:数据清洗不是一次性工作,而是贯穿整个建模流程的迭代过程。每次你发现模型表现不对劲,第一反应应该是——回去看看数据。

3.1 缺失值检测:别让“空”坑了你

外汇市场是24小时交易的,但并不意味着每一秒都有数据。节假日、流动性枯竭、数据源断连,都会产生缺失值。我刚开始做的时候,以为缺失值就是简单的NaN,后来发现事情没那么简单。

缺失值的三种类型:

  • 完全随机缺失(MCAR): 比如网络波动导致某几秒数据没传过来。这种最“善良”,直接删掉或简单填充就行。
  • 随机缺失(MAR): 比如非农数据公布前后,流动性骤降,部分报价缺失。这种需要小心,因为它和市场状态有关。
  • 非随机缺失(MNAR): 比如某个货币对在特定时间段内完全没有交易。这种最麻烦,往往意味着市场结构发生了变化。

怎么检测?我习惯先看整体缺失率,再看连续缺失的长度。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('eurusd_2024.csv', parse_dates=['timestamp'])
df.set_index('timestamp', inplace=True)

# 1. 整体缺失率
missing_rate = df.isnull().sum() / len(df)
print("各字段缺失率:")
print(missing_rate)

# 2. 连续缺失检测
def consecutive_missing(series, max_gap=5):
    """检测连续缺失超过max_gap的位置"""
    is_missing = series.isnull()
    # 找到连续缺失的起始和结束
    missing_groups = (is_missing != is_missing.shift()).cumsum()
    consecutive = is_missing.groupby(missing_groups).sum()
    return consecutive[consecutive > max_gap]

# 检查bid价格连续缺失超过5个的情况
bad_segments = consecutive_missing(df['bid'], max_gap=5)
print(f"\n连续缺失超过5个的段数:{len(bad_segments)}")

我的经验:外汇数据里,连续缺失超过10个tick(大约1秒)就要警惕了。我曾经遇到过一个案例,某数据商在伦敦收盘前后会“漏掉”整整30秒的数据,导致我的做市模型在关键时段完全失效。

3.2 异常值识别:那些“离谱”的价格

外汇市场虽然流动性好,但异常值并不少见。胖手指、数据商错误、流动性瞬间枯竭,都会产生离谱的价格。你想想看,EUR/USD正常波动在1.1000-1.1100之间,突然冒出一个1.2000的报价——这明显有问题。

常用的异常值检测方法:

  • Z-score方法: 假设数据服从正态分布,超过3个标准差就算异常。但外汇数据往往有厚尾,这个方法容易漏掉。
  • IQR方法: 用四分位距,鲁棒性更好。我比较推荐这个。
  • 滚动窗口法: 用过去N个tick的均值和标准差来判断当前值是否异常。适合实时检测。
def detect_outliers_iqr(series, multiplier=3):
    """使用IQR方法检测异常值"""
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    
    outliers = (series < lower_bound) | (series > upper_bound)
    return outliers

# 检测bid价格的异常值
outliers = detect_outliers_iqr(df['bid'], multiplier=3)
print(f"检测到异常值数量:{outliers.sum()}")
print(f"异常值占比:{outliers.sum() / len(df) * 100:.4f}%")

# 看看这些异常值长什么样
print("\n异常值样本:")
print(df[outliers].head())

注意:不要机械地删除所有异常值。有些“异常”其实是市场真实状态——比如非农数据公布瞬间的剧烈波动。我建议先标记,再结合业务逻辑判断。

3.3 重复数据处理:别让“复制粘贴”毁了你的模型

重复数据在外汇数据里很常见。数据商重传、系统重连、或者某些API的bug,都会导致同一笔数据出现多次。如果不处理,你的模型会“过度学习”这些重复样本,导致偏差。

重复数据的两种类型:

  • 完全重复: 所有字段都一样。这种直接删掉就行。
  • 部分重复: 时间戳相同但价格不同。这种更麻烦,需要判断哪个是“真”的。
# 1. 检测完全重复
full_duplicates = df.duplicated(keep='first')
print(f"完全重复行数:{full_duplicates.sum()}")

# 2. 检测时间戳重复(部分重复)
timestamp_duplicates = df.index.duplicated(keep='first')
print(f"时间戳重复行数:{timestamp_duplicates.sum()}")

# 3. 处理时间戳重复:取平均值或保留最后一条
# 方法一:保留最后一条
df_deduped = df[~df.index.duplicated(keep='last')]

# 方法二:对同一时间戳的多个报价取平均
df_avg = df.groupby(df.index).mean()

print(f"\n去重前数据量:{len(df)}")
print(f"去重后数据量(保留最后一条):{len(df_deduped)}")
print(f"去重后数据量(取平均):{len(df_avg)}")

我的习惯:对于做市数据,我倾向于保留最后一条。因为外汇市场里,最新的报价往往是最“真实”的——它反映了市场最新的成交意愿。取平均反而可能引入“假”价格。

3.4 知识体系:数据质量初探全景图

下面这张图,是我自己总结的数据质量初探流程。你可以把它当作一个检查清单,每次拿到新数据,按这个流程走一遍,基本不会出大问题。

数据质量初探流程 原始数据 缺失值检测 异常值识别 重复数据处理 清洗后数据 整体缺失率 连续缺失检测 Z-score / IQR 滚动窗口法 完全重复 时间戳重复 删除 / 填充 前向/后向填充 标记 / 修正 业务逻辑判断 删除重复行 取平均/保留最后

3.5 实战建议:先跑一遍,再谈建模

说实话,很多做量化的人,拿到数据第一件事就是跑模型。这是大忌。我个人的工作流是这样的:

  1. 先跑一个快速的质量报告: 用上面那些代码,看看缺失率、异常值比例、重复数据量。
  2. 可视化检查: 把价格序列画出来,肉眼扫一遍。有些异常,算法检测不出来,但人眼一看就知道不对劲。
  3. 记录清洗日志: 每次清洗,都记录删了多少数据、填充了多少、为什么这么处理。这样以后模型出问题,还能回溯。

避坑指南:我曾经因为没处理重复数据,导致一个做市策略的回测年化收益率虚高了15%。后来发现,那些“超额收益”全来自重复的tick数据——模型在同一个价格上反复交易,产生了虚假的利润。嗯,从那以后,重复数据检测成了我每次清洗的第一步。

数据质量这件事,说白了就是“磨刀不误砍柴工”。你花30%的时间在数据清洗上,后面建模和回测能省下70%的调试时间。这笔账,怎么算都划算。

专注资料整理