4. 数据质量初探:缺失值、异常值、重复值、跳空数据的识别与统计
做高频做市,数据质量就是命根子。我见过太多团队,策略逻辑写得天花乱坠,结果一上实盘就崩,查到最后——数据源里一堆脏数据。今天咱们就来聊聊,怎么给期货高频数据做个全面体检。
说白了,数据清洗不是炫技,是保命。你想想看,如果拿到的 tick 数据里混着跳空、重复、缺失,那算出来的特征全是错的,策略能不亏钱吗?
核心原则:先清洗,后计算。宁可少数据,不要脏数据。
4.1 缺失值识别与处理
高频数据里的缺失值,通常不是空值,而是时间戳跳过了某个 tick。比如 09:30:00.000 之后直接到了 09:30:00.050,中间缺了 10 毫秒的数据。
我个人习惯用 pandas 的 isna() 先扫一遍,但更关键的是检查时间序列的连续性。
import pandas as pd
import numpy as np
# 假设 df 包含 'timestamp' 和 'last_price' 列
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.set_index('timestamp')
# 检查缺失值数量
missing_count = df['last_price'].isna().sum()
print(f"缺失值数量: {missing_count}")
# 检查时间间隔是否均匀
time_diff = df.index.to_series().diff().dt.total_seconds()
irregular_gaps = time_diff[time_diff > 0.001] # 超过1毫秒的间隔
print(f"异常间隔数量: {len(irregular_gaps)}")
我的经验:我在处理螺纹钢主力合约时发现,夜盘收盘前后经常出现 3-5 秒的数据断层。这不是数据源的问题,是交易所撮合机制导致的。这种断层不能简单填充,得单独标记。
处理缺失值,我一般分三步走:
- 标记法:加一列
is_missing,保留原始信息 - 前向填充:用上一个有效值填充,适合价格数据
- 插值法:线性插值,适合成交量这类连续变化的数据
4.2 异常值检测
异常值在高频数据里太常见了。我记得有一次,某合约的最后一笔成交价突然从 3800 跳到 4200,然后又跳回来。后来查证是某机构的大单误操作。
常用的方法有几种:
- Z-score 法:适合正态分布的数据,但高频数据往往不是正态的
- IQR 法:基于四分位距,更稳健
- 百分比法:比如价格变动超过 5% 就标记为异常
def detect_outliers_iqr(series, multiplier=3):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - multiplier * IQR
upper_bound = Q3 + multiplier * IQR
return (series < lower_bound) | (series > upper_bound)
# 对价格变动率做异常检测
df['price_change'] = df['last_price'].pct_change()
outliers = detect_outliers_iqr(df['price_change'].dropna())
print(f"异常值数量: {outliers.sum()}")
避坑指南:我曾经直接用 Z-score 检测股指期货的 tick 数据,结果把开盘集合竞价阶段的正常波动全标记成了异常。后来改用 IQR 法,配合时间窗口(比如只检测连续 3 笔内的异常),效果才好很多。
4.3 重复值处理
重复值在高频数据里很隐蔽。不是简单的两行一模一样,而是时间戳相同但价格不同,或者价格相同但时间戳差了几微秒。
我一般这样处理:
# 检查完全重复的行
duplicates_exact = df.duplicated(keep='first')
print(f"完全重复行数: {duplicates_exact.sum()}")
# 检查时间戳重复但价格不同的情况
timestamp_dupes = df.index.duplicated(keep=False)
conflict_dupes = df[timestamp_dupes].groupby(level=0).filter(lambda x: x['last_price'].nunique() > 1)
print(f"时间戳冲突行数: {len(conflict_dupes)}")
处理策略:
- 完全重复:保留第一条,删除其余
- 时间戳冲突:取价格均值,或者保留最后一条(看业务逻辑)
- 微秒级重复:如果时间差小于 1 微秒,合并处理
4.4 跳空数据识别
跳空是高频做市的大敌。你想想看,上一笔价格是 3800,下一笔直接跳到 3850,中间没有任何成交。这种数据如果不处理,算出来的买卖价差、波动率全是错的。
识别跳空的核心逻辑:
# 计算价格跳空幅度
df['price_jump'] = abs(df['last_price'].diff())
threshold = df['last_price'].median() * 0.01 # 1% 阈值
jumps = df[df['price_jump'] > threshold]
print(f"跳空次数: {len(jumps)}")
# 同时检查时间间隔
jump_with_gap = jumps[time_diff.loc[jumps.index] > 0.1] # 超过100毫秒的跳空
print(f"带时间断层的跳空: {len(jump_with_gap)}")
实战技巧:我在做原油期货时发现,跳空往往发生在重要数据公布前后。比如 EIA 库存数据公布的那一秒,价格可能跳 2%。这种跳空不能直接删除,得单独标记,作为特征输入到模型里。
4.5 数据质量报告
每次清洗完数据,我都会生成一份质量报告。这不仅是给自己看,也是给团队和风控部门看的。
| 检查项 | 统计指标 | 阈值 | 处理方式 |
|---|---|---|---|
| 缺失值 | 缺失比例 | < 5% | 前向填充 |
| 异常值 | IQR 倍数 | 3 倍 IQR | 标记或剔除 |
| 重复值 | 重复比例 | < 1% | 去重合并 |
| 跳空数据 | 跳空次数 | 按品种设定 | 标记为特征 |
def generate_quality_report(df):
report = {}
report['total_rows'] = len(df)
report['missing_rate'] = df['last_price'].isna().mean()
report['duplicate_rate'] = df.duplicated().mean()
report['outlier_count'] = detect_outliers_iqr(df['last_price']).sum()
report['jump_count'] = len(df[df['price_jump'] > threshold])
return report
report = generate_quality_report(df)
for key, value in report.items():
print(f"{key}: {value}")
记住:数据质量报告不是一次性工作。每次接入新数据源、每次换合约,都得重新跑一遍。我见过有人用半年前的数据质量报告来评估新数据,结果亏得一塌糊涂。
4.6 本章知识体系
下面这张图,是我自己总结的数据质量初探流程。你可以把它贴在工位上,每次清洗数据前看一眼。
嗯,到这里,数据质量初探就讲完了。记住,清洗不是目的,目的是让数据真实反映市场行为。下一章咱们会深入特征工程,但前提是——你的数据得干净。
公众号:蓝海数据掘金营,微信 deep3321