4. 数据质量初探:缺失值、异常值、重复值、跳空数据的识别与统计

做高频做市,数据质量就是命根子。我见过太多团队,策略逻辑写得天花乱坠,结果一上实盘就崩,查到最后——数据源里一堆脏数据。今天咱们就来聊聊,怎么给期货高频数据做个全面体检。

说白了,数据清洗不是炫技,是保命。你想想看,如果拿到的 tick 数据里混着跳空、重复、缺失,那算出来的特征全是错的,策略能不亏钱吗?

核心原则:先清洗,后计算。宁可少数据,不要脏数据。

4.1 缺失值识别与处理

高频数据里的缺失值,通常不是空值,而是时间戳跳过了某个 tick。比如 09:30:00.000 之后直接到了 09:30:00.050,中间缺了 10 毫秒的数据。

我个人习惯用 pandasisna() 先扫一遍,但更关键的是检查时间序列的连续性。

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,然后又跳回来。后来查证是某机构的大单误操作。

常用的方法有几种:

  1. Z-score 法:适合正态分布的数据,但高频数据往往不是正态的
  2. IQR 法:基于四分位距,更稳健
  3. 百分比法:比如价格变动超过 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 本章知识体系

下面这张图,是我自己总结的数据质量初探流程。你可以把它贴在工位上,每次清洗数据前看一眼。

数据质量初探流程 原始 Tick 数据 缺失值检查 时间连续性 异常值检测 IQR / Z-score 重复值处理 时间戳去重 跳空识别 价格断层 清洗处理(填充/标记/剔除) 生成数据质量报告

嗯,到这里,数据质量初探就讲完了。记住,清洗不是目的,目的是让数据真实反映市场行为。下一章咱们会深入特征工程,但前提是——你的数据得干净。


公众号:蓝海数据掘金营,微信 deep3321