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 最小者。去重前后记录数之差,就是报告中的"重复剔除量"。
六、质量报告:让清洗过程可追溯、可门控
清洗的最后一步是把上述四类处理的结果固化为报告。报告按"交易日 × 品种"粒度组织,至少包含:总记录数、关键字段缺失数、异常标记数与修复数、时间倒序数、重复剔除数、隔离区记录数及占比。更重要的是设置门控阈值——例如隔离区占比超过 0.5%(示例阈值,需按品种流动性校准),说明当日数据源存在系统性问题,应阻断下游漂移特征计算并触发告警,而不是带着高污染数据继续跑模型。
下面是一份可直接套用的报告字段示例(数值为教学假设,非实测):
| 指标 | 示例值 | 判定 |
|---|---|---|
| 总记录数 | 1,284,600 | — |
| 关键字段缺失 | 312 | 占比 0.024%,正常 |
| 异常标记 / 已修复 | 1,045 / 987 | 修复率 94.4%,正常 |
| 时间倒序 | 58 | 毫秒级抖动,已重排 |
| 重复剔除 | 2,310 | 多路合并所致,正常 |
| 隔离区占比 | 0.031% | 低于 0.5% 门控,放行 |
clean_v1.3)写入报告与清洗后数据的元信息。当漂移识别结果出现突变时,先核对规则版本是否变更,能快速排除"清洗口径变化"这一伪漂移来源。完成本节流水线后,读者应得到三样东西:一份口径明确的清洗规则集、一个可重复执行的清洗脚本、以及一份带门控判断的质量报告。这三者共同保证进入漂移识别环节的数据是"可解释的干净",而不是"看起来没空值的干净"。