4. 逐笔数据质量诊断与清洗实战

逐笔成交数据是刻画订单流微观结构的基础原料,但它从交易所落地到本地数据库的过程中,几乎必然经历缺失、异常、时间错位和重复四类污染。如果直接把这些"脏"数据喂给漂移识别模型,模型学到的可能不是市场结构的变化,而是采集链路和存储环节的噪声。本节围绕一条可复现的清洗流水线展开:先定义每类问题的判定口径,再给出具体的检测与修复操作,最后把处理结果沉淀为一份可追溯的数据质量报告。

一、先明确"干净"的标准:字段与口径

清洗之前必须固定字段语义。以常见的逐笔成交表为例,核心字段包括:trade_id(交易所成交编号)、symbol、trade_time(精确到毫秒或微秒的成交时刻)、price、volume、amount、side(主动买卖方向)、seq_no(行情序列号)。判定规则要写死:trade_id 在同一交易日内唯一;trade_time 必须落在该品种连续竞价时段内;price 必须处于当日涨跌停价之间且为最小价位单位的整数倍;volume 为正整数且不超过单笔最大申报量;amount 与 price×volume 的偏差应在容差内(考虑成交金额取整规则)。这些口径是后续所有检测的基准,也是质量报告里"规则命中数"的来源。

二、缺失值:区分"真缺失"与"结构性空值"

逐笔数据的缺失有两种性质完全不同的情况。第一种是结构性空值,例如集合竞价阶段没有连续逐笔、某笔成交的 side 字段在部分行情源中本就不下发,这类缺失不应填补,而应标记为"不适用"。第二种是真缺失,例如 price 或 volume 为空、trade_time 缺失,这类记录无法参与任何计算,必须处理。

实操中按字段分级处理:关键字段(trade_id、trade_time、price、volume)任一为空,整条记录进入隔离区并计数;非关键字段(side、seq_no)为空时保留记录,用哨兵值填充(如 side 填 "U" 表示未知),并在报告中单列。切忌对价格做前向填充——逐笔价格是瞬时状态,用上一笔价格填补会人为制造零收益,直接污染漂移特征。下面是一段基于 pandas 的检测示例:

import pandas as pd
key_cols = ["trade_id", "trade_time", "price", "volume"]
mask_bad = df[key_cols].isna().any(axis=1)
quarantine = df[mask_bad].copy()
clean = df[~mask_bad].copy()
clean["side"] = clean["side"].fillna("U")
print("关键字段缺失记录数:", len(quarantine))

三、异常值:用业务边界而非统计阈值

逐笔数据的异常值检测,业务规则优先于 3σ 之类的统计方法。因为价格分布本身厚尾,统计阈值会误杀真实的大额成交。推荐三条硬规则:其一,价格越界——price 超出当日涨停价或跌停价,或不是最小变动价位的整数倍;其二,量价不匹配——|amount - price×volume| / amount 超过 0.5%(示例容差,需按实际取整规则校准);其三,量能越界——volume 为 0、为负,或超过该品种单笔申报上限。

命中规则的记录不要直接删除,而是先打标签、再判断。例如价格越界但量价自洽的记录,可能是复权或代码映射错误,应回溯数据源;量价不匹配但价格合法的记录,更可能是金额字段精度问题,可尝试用 price×volume 重算金额并标记"已修正"。只有无法修复的记录才移入隔离区。这种"标记—修复—隔离"的三级处理,能让质量报告说清楚每条异常的去向。

四、时间戳对齐:统一时区、精度与单调性

时间戳问题最隐蔽,也最影响漂移识别。常见三类:时区混用(部分源为 UTC、部分为本地时间)、精度不一致(毫秒与微秒混存)、以及同一 seq_no 内时间非单调。处理顺序应为:先统一到同一时区并转为带时区的时间类型,再统一精度到微秒,最后按 seq_no 排序检查 trade_time 是否单调不减。

df["trade_time"] = (pd.to_datetime(df["trade_time"], utc=True)
                      .dt.tz_convert("Asia/Shanghai"))
df = df.sort_values(["symbol", "seq_no"])
non_mono = df.groupby("symbol")["trade_time"].diff().dt.total_seconds() < 0
print("时间倒序记录数:", non_mono.sum())

对于时间倒序的记录,若 seq_no 连续且偏差在数毫秒内,通常是撮合与行情推送的时钟抖动,可按 seq_no 顺序重排并保留原值;若偏差达到秒级,则说明存在跨源拼接错误,应整段隔离。注意:本节所有时间比较均以"交易所本地时间、单调不减"为参照约定,后续特征计算不得再切换参照系。

五、重复记录:主键去重与近似去重

重复分两种。完全重复:所有字段一致,直接用 trade_id 去重,保留首条。近似重复:trade_id 不同但 symbol、trade_time、price、volume 完全相同,多由多路行情合并或断线重连补发造成。近似重复不能简单按主键删,否则会误删真实的同价同量连续成交。稳妥做法是:先按 trade_id 去重,再在 (symbol, trade_time, price, volume) 分组内,若组内 seq_no 差值小于阈值(如 3)且时间差小于 1 毫秒,判定为重复并保留 seq_no 最小者。去重前后记录数之差,就是报告中的"重复剔除量"。

逐笔数据清洗流水线与质量门控 原始逐笔数据 trade_id / time / px / vol ① 缺失值分级 关键字段→隔离 非关键→哨兵填充 禁止价格前向填充 ② 异常值判定 价格越界 / 量价偏差 量能越界 标记→修复→隔离 ③ 时间戳对齐 统一时区→统一精度 按 seq_no 查单调性 秒级偏差整段隔离 ④ 重复记录处理 trade_id 主键去重 近似重复按 seq_no 差值阈值判定 隔离区 不可修复记录 保留原始值待回溯 清洗后数据集 单调时间 + 唯一主键 字段口径一致 数据质量报告(按交易日 × 品种) 总记录数 / 关键缺失数 / 异常标记数 / 修复数 时间倒序数 / 重复剔除数 / 隔离区占比 门控阈值:隔离占比 ≤ 0.5%,否则阻断下游特征计算

六、质量报告:让清洗过程可追溯、可门控

清洗的最后一步是把上述四类处理的结果固化为报告。报告按"交易日 × 品种"粒度组织,至少包含:总记录数、关键字段缺失数、异常标记数与修复数、时间倒序数、重复剔除数、隔离区记录数及占比。更重要的是设置门控阈值——例如隔离区占比超过 0.5%(示例阈值,需按品种流动性校准),说明当日数据源存在系统性问题,应阻断下游漂移特征计算并触发告警,而不是带着高污染数据继续跑模型。

下面是一份可直接套用的报告字段示例(数值为教学假设,非实测):

指标示例值判定
总记录数1,284,600—
关键字段缺失312占比 0.024%,正常
异常标记 / 已修复1,045 / 987修复率 94.4%,正常
时间倒序58毫秒级抖动,已重排
重复剔除2,310多路合并所致,正常
隔离区占比0.031%低于 0.5% 门控,放行
建议把清洗规则版本号(如 clean_v1.3)写入报告与清洗后数据的元信息。当漂移识别结果出现突变时,先核对规则版本是否变更,能快速排除"清洗口径变化"这一伪漂移来源。
常见误区:为追求"零缺失"而对价格、成交量做插值或均值填充。逐笔数据的每一行都是一次真实撮合,插值会凭空制造不存在的成交,使订单流不平衡等特征失真。正确做法是隔离而非编造。

完成本节流水线后,读者应得到三样东西:一份口径明确的清洗规则集、一个可重复执行的清洗脚本、以及一份带门控判断的质量报告。这三者共同保证进入漂移识别环节的数据是"可解释的干净",而不是"看起来没空值的干净"。