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 知识体系:数据质量初探全景图
下面这张图,是我自己总结的数据质量初探流程。你可以把它当作一个检查清单,每次拿到新数据,按这个流程走一遍,基本不会出大问题。
3.5 实战建议:先跑一遍,再谈建模
说实话,很多做量化的人,拿到数据第一件事就是跑模型。这是大忌。我个人的工作流是这样的:
- 先跑一个快速的质量报告: 用上面那些代码,看看缺失率、异常值比例、重复数据量。
- 可视化检查: 把价格序列画出来,肉眼扫一遍。有些异常,算法检测不出来,但人眼一看就知道不对劲。
- 记录清洗日志: 每次清洗,都记录删了多少数据、填充了多少、为什么这么处理。这样以后模型出问题,还能回溯。
避坑指南:我曾经因为没处理重复数据,导致一个做市策略的回测年化收益率虚高了15%。后来发现,那些“超额收益”全来自重复的tick数据——模型在同一个价格上反复交易,产生了虚假的利润。嗯,从那以后,重复数据检测成了我每次清洗的第一步。
数据质量这件事,说白了就是“磨刀不误砍柴工”。你花30%的时间在数据清洗上,后面建模和回测能省下70%的调试时间。这笔账,怎么算都划算。