3、数据预处理:数据清洗、去重、缺失值处理、时间戳标准化、交易日过滤等实战技巧

逐笔成交数据是量化交易与市场微观结构研究的“原料”,但其原始形态往往充满噪声:字段错位、重复记录、时间戳格式混乱、非交易时段残留数据等问题层出不穷。若未经清洗直接进入特征提取环节,轻则导致特征计算偏差,重则使回测结果失真甚至策略失效。本章将围绕数据清洗、去重、缺失值处理、时间戳标准化与交易日过滤五大核心环节,结合沪深交易所Level-2行情的实际数据特征,给出可落地的处理流程与判断标准。

逐笔成交数据预处理链路 原始逐笔成交 Level-2 / Tick级 ① 数据清洗 字段校验 / 格式修正 ② 去重 主键去重 / 语义去重 ③ 缺失值处理 删除 / 填充 / 标记 ④ 时间戳标准化 统一时区 / 精度对齐 ⑤ 交易日过滤 剔除节假日/非交易时段 干净逐笔数据 可进入特征提取 关键输出指标: • 清洗后数据完整率 ≥ 99.5%(原始数据完整率通常为 98%~99.8%) • 去重后记录数减少比例:正常 0.1%~0.5%,异常时可达 5% 以上 • 时间戳标准化精度:统一到毫秒(ms),误差 ±1ms 以内 • 交易日过滤后数据量:仅保留 9:30-11:30、13:00-15:00 的连续竞价与集合竞价记录 • 最终输出:结构化 DataFrame / Parquet 文件,字段类型统一,索引唯一

3.1 数据清洗:字段校验与格式修正

逐笔成交数据通常以CSV、二进制或数据库表形式存储,常见字段包括:交易时间、证券代码、成交价格、成交数量、成交金额、买卖方向、成交编号等。数据清洗的首要任务是确保每个字段的值符合业务逻辑与数据类型约束。

核心原理

数据清洗的本质是“约束校验”。每一列都有其合法值域:价格必须为正数且不超过当日涨跌停范围;数量必须为正整数;买卖方向只能为“买”“卖”或“未知”;时间戳必须落在交易时段内。清洗过程就是将这些约束转化为过滤规则,逐条检查并修正或剔除异常记录。

判断方法与操作流程

  1. 字段类型检查:确认价格、数量为数值类型,时间戳为日期时间类型,证券代码为字符串或整数。
  2. 范围校验:价格是否在 [0.01, 涨跌停价] 区间;数量是否大于0;金额是否等于价格×数量(允许±0.01元舍入误差)。
  3. 逻辑一致性:成交编号是否唯一且递增;买卖方向是否与成交类型匹配。
  4. 格式统一:证券代码统一为6位数字(如“600000”而非“600000.SH”或“600000.SS”);时间戳统一为“YYYY-MM-DD HH:MM:SS.mmm”格式。
实战经验:沪深交易所Level-2逐笔数据中,价格字段偶尔会出现“0.00”或负值,这通常是交易所测试数据或异常交易所致。建议直接剔除,而非填充,因为这类数据不具备市场含义。

案例拆解:某券商Level-2数据清洗

某日处理某只股票的逐笔成交数据,原始记录12,845条。清洗后发现:

  • 价格字段有3条记录为0.00,属于交易所盘后测试数据,直接删除。
  • 数量字段有1条记录为-500,属于数据源编码错误,删除。
  • 金额字段有12条记录与价格×数量不一致,偏差在0.01~0.03元之间,属于四舍五入差异,保留但标记。

清洗后有效记录12,841条,完整率99.97%。

3.2 去重:主键去重与语义去重

逐笔数据在传输或存储过程中可能产生重复记录。重复数据会直接导致成交量、成交笔数等特征被高估,必须严格处理。

核心原理

去重分为两个层次:主键去重和语义去重。主键去重基于唯一标识(如“成交编号+证券代码+交易时间”),删除完全相同的记录;语义去重则针对主键不同但业务含义相同的记录,例如同一笔成交被拆分为两条记录但价格、数量、时间完全一致。

操作流程

去重层级 判断依据 处理方式 适用场景
主键去重 成交编号 + 证券代码 + 交易时间 保留第一条,删除后续重复 交易所原始数据
语义去重 价格 + 数量 + 时间(±1ms)+ 方向 合并为一条,数量累加 多源数据合并场景
风险提示:语义去重需谨慎。若两笔成交时间相差1ms以内且价格、数量相同,可能是真实的两笔独立成交,而非重复记录。建议先检查成交编号是否不同,若编号不同则不应合并。

3.3 缺失值处理:删除、填充与标记

逐笔数据中的缺失值通常表现为空字符串、NaN或NULL。缺失值处理策略取决于缺失比例、字段重要性和业务场景。

处理策略

  • 删除记录:当关键字段(价格、数量、时间)缺失时,直接删除该条记录。适用于缺失比例低于0.5%的场景。
  • 填充默认值:对于非关键字段(如成交编号),可用前一条记录的值填充,或填充为“未知”。
  • 标记处理:在数据集中增加“is_missing”列,标记缺失字段,便于后续特征提取时单独处理。
经验法则:若某字段缺失比例超过5%,应回溯数据源,检查是采集环节还是传输环节出现问题,而非简单填充。

3.4 时间戳标准化:统一时区与精度

逐笔数据的时间戳是特征提取的核心维度。不同数据源可能使用不同时区(北京时间、UTC)、不同精度(秒、毫秒、微秒)或不同格式(字符串、Unix时间戳)。标准化是确保后续时间窗口计算(如1分钟K线、5秒切片)准确的前提。

标准化步骤

  1. 统一时区:全部转换为北京时间(Asia/Shanghai),UTC时间需加8小时。
  2. 统一精度:统一到毫秒(ms)。若原始精度为微秒,则四舍五入到毫秒;若为秒,则补足毫秒位为000。
  3. 统一格式:转换为“YYYY-MM-DD HH:MM:SS.mmm”字符串或对应的datetime类型。
# Python示例:时间戳标准化
import pandas as pd

df['trade_time'] = pd.to_datetime(df['trade_time'], unit='ms', utc=True)
df['trade_time'] = df['trade_time'].dt.tz_convert('Asia/Shanghai')
df['trade_time'] = df['trade_time'].dt.strftime('%Y-%m-%d %H:%M:%S.%f').str[:-3]

3.5 交易日过滤:剔除无效时段

A股交易时段为周一至周五的9:30-11:30和13:00-15:00(法定节假日除外)。逐笔数据中常包含集合竞价(9:15-9:25)、午间休市(11:30-13:00)以及盘后固定价格交易等非连续竞价时段记录。特征提取前需明确过滤规则。

过滤规则

时段 是否保留 说明
9:15-9:25 集合竞价 保留(可选) 包含开盘价形成信息,但需单独标记
9:30-11:30 连续竞价 保留 核心交易时段
11:30-13:00 午间休市 剔除 无成交记录,但可能有废单
13:00-15:00 连续竞价 保留 核心交易时段
15:00-15:30 盘后固定价格 剔除(默认) 若研究大宗交易可单独处理
实战建议:交易日历应使用交易所官方发布的数据,而非简单判断周末。例如,国庆节调休时周六可能为交易日,而周日可能休市。建议维护一张交易日历表,与逐笔数据关联过滤。

3.6 完整预处理流程与质量校验

将上述五个环节串联,形成标准预处理流水线。每一步操作后都应记录数据量变化,便于追溯。

流程清单

  1. 读取原始数据,记录初始记录数N0。
  2. 数据清洗:剔除价格≤0、数量≤0、金额不匹配的记录,记录剩余N1。
  3. 主键去重:按“成交编号+证券代码+时间”去重,记录剩余N2。
  4. 缺失值处理:删除关键字段缺失的记录,记录剩余N3。
  5. 时间戳标准化:统一时区与精度,检查时间戳是否单调递增。
  6. 交易日过滤:剔除非交易日与非交易时段记录,记录最终N4。
  7. 质量校验:计算各步骤数据损失比例,生成预处理报告。
常见误区:部分开发者直接使用“时间在9:30-11:30或13:00-15:00”作为过滤条件,但忽略了集合竞价时段。若研究开盘微观结构,集合竞价数据不可丢弃;若仅研究连续竞价,则应明确剔除9:25-9:30之间的记录。

3.7 案例复盘:某私募量化团队预处理实战

某私募团队获取某日全市场Level-2逐笔数据,原始文件约8GB,涉及5000余只股票。初始记录数约1.2亿条。预处理目标:生成可用于分钟级特征提取的干净数据集。

初始现象:数据中存在大量时间戳为“1970-01-01”的记录(Unix时间戳为0),以及部分股票代码为“000000”的测试数据。

分析判断:Unix时间戳为0的记录属于数据源初始化异常,应直接剔除;股票代码为“000000”的记录为交易所测试数据,同样剔除。

实际操作:

  • 清洗阶段:剔除价格≤0的记录约3万条,剔除数量≤0的记录约1.2万条。
  • 去重阶段:按主键去重,发现约0.3%的重复记录,主要源于数据源重复推送。
  • 缺失值处理:成交编号缺失的记录约500条,直接删除。
  • 时间戳标准化:将Unix毫秒时间戳转换为北京时间,并检查时间单调性。
  • 交易日过滤:剔除周末与节假日数据,仅保留连续竞价时段。

结果对比:最终保留有效记录约1.15亿条,数据完整率95.8%。预处理耗时约25分钟(使用Pandas并行处理)。

复盘经验:预处理环节中,时间戳标准化耗时最长,建议使用向量化操作而非逐行循环;交易日过滤应提前准备交易日历表,避免每次重复判断。

总结:数据预处理不是“一次性”工作,而是需要根据数据源变化持续迭代的工程环节。建议将预处理逻辑封装为函数或类,并记录每次运行的参数与数据量变化,形成可审计的数据血缘。