1. 逐笔成交数据字段解析与基础清洗

原始逐笔成交表长什么样

逐笔成交(Trade Tick)是交易所撮合引擎每发生一次成交就推送的一条记录。不同数据源字段名不同,但语义基本固定为六类:成交时间(时间戳,可能带毫秒/微秒)、成交编号(唯一序号)、证券代码、成交价格、成交数量、买卖方向(主动买/主动卖,或 BS 标志)。本案例用一份教学示例 CSV 演示,字段为 trade_time, trade_id, symbol, price, volume, side,其中故意埋入三类脏数据:时间戳缺失、时间戳格式不统一(部分为 HH:MM:SS 纯时间)、方向字段为空或写成 B/S/买/卖 混用。

清洗规则与字段映射

清洗目标不是"删掉脏行",而是把每条记录变成可被后续特征组合直接消费的规整行。规则如下:

  • 时间戳:统一解析为 datetime64[ns]。纯时间(无日期)按当日交易日补齐;无法解析或为空的记录标记为 NaT 并剔除。
  • 价格/数量:强制转数值,price <= 0 或 volume <= 0 视为异常剔除(交易所不会推送零价零量成交)。
  • 方向:映射为 +1(主动买)/ -1(主动卖);空值或无法识别记为 0 并单独计数,不直接丢弃,便于后续判断数据源质量。
  • 排序与去重:按 trade_time 升序,trade_id 去重,保证时序单调。
逐笔成交原始表 → 清洗 → 规整表 原始 CSV(含脏数据) trade_time trade_id price volume side 09:30:01.250 1001 12.35 300 B 09:30:01.400 1002 12.36 200 (空) (空) 1003 12.34 500 S 09:30:02 1004 0.00 100 买 清洗规则 ① 时间解析 → datetime64 ② price/volume 转数值 ③ side → +1 / -1 / 0 ④ 剔除 NaT、非正价量 ⑤ 按时间排序 + trade_id 去重 规整表(可直接用) trade_time price volume side 09:30:01.250 12.35 300 +1 09:30:01.400 12.36 200 0 09:30:02.000 12.34 500 -1 (1004 因 price=0 被剔除) 字段语义与后续用途 trade_time:成交发生时刻,后续用于计算时间间隔、聚合窗口、与委托流对齐。 price / volume:单笔成交价与量,是成交金额、VWAP、大单识别的基础输入。 side:+1 主动买 / -1 主动卖 / 0 未知,决定带符号成交金额的方向。 trade_id:唯一键,用于去重与跨表关联,不参与特征计算但必须保留。 注意:side=0 的记录保留但需在特征阶段单独处理,避免方向符号化时被误判为中性。

用 pandas 完成清洗

下面代码可直接运行,输入为教学示例 CSV 字符串,输出规整 DataFrame。关键点:pd.to_datetime 的 errors='coerce' 把无法解析的时间变成 NaT;纯时间用 format='%H:%M:%S' 解析后补日期;方向映射用字典 + map,未命中填 0。

import pandas as pd
import io

raw = """trade_time,trade_id,symbol,price,volume,side
09:30:01.250,1001,600000,12.35,300,B
09:30:01.400,1002,600000,12.36,200,
,1003,600000,12.34,500,S
09:30:02,1004,600000,0.00,100,买
09:30:02.100,1005,600000,12.37,400,卖
09:30:02.100,1005,600000,12.37,400,卖
"""

df = pd.read_csv(io.StringIO(raw), dtype=str)

# 1) 时间解析:先尝试完整时间,再尝试纯时间补当日
t_full = pd.to_datetime(df["trade_time"], errors="coerce")
t_only = pd.to_datetime(df["trade_time"], format="%H:%M:%S", errors="coerce")
base = pd.Timestamp("2024-06-03")  # 教学示例交易日
t_only = t_only.apply(lambda x: base + (x - x.normalize()) if pd.notna(x) else pd.NaT)
df["trade_time"] = t_full.fillna(t_only)

# 2) 数值转换
df["price"] = pd.to_numeric(df["price"], errors="coerce")
df["volume"] = pd.to_numeric(df["volume"], errors="coerce")

# 3) 方向映射
side_map = {"B": 1, "买": 1, "S": -1, "卖": -1}
df["side"] = df["side"].fillna("").str.strip().map(side_map).fillna(0).astype(int)

# 4) 剔除异常:时间缺失、非正价量
before = len(df)
df = df.dropna(subset=["trade_time", "price", "volume"])
df = df[(df["price"] > 0) & (df["volume"] > 0)]

# 5) 排序 + 去重
df = df.sort_values("trade_time").drop_duplicates("trade_id").reset_index(drop=True)

print(f"原始 {before} 行,清洗后 {len(df)} 行")
print(df[["trade_time", "trade_id", "price", "volume", "side"]])

预期输出(教学示例,非实测):原始 6 行,清洗后 4 行。1003 因时间缺失被剔除,1004 因 price=0 被剔除,1005 重复行被去重,1002 的 side 为空映射为 0 但保留。

清洗结果的自检清单

检查项通过条件本例结果
时间列类型datetime64[ns],无 NaT通过
时间单调性trade_time 严格非降通过
价格/数量全部 > 0通过
方向取值仅含 {-1, 0, 1}通过(含 1 条 0)
主键唯一trade_id 无重复通过
若数据源时间戳为微秒级整数(如 1717380601250000),用 pd.to_datetime(col, unit="us") 解析,不要先转字符串再解析,避免精度丢失。方向字段若为 0/1 或 B/S 混用,先统一 str.upper().str.strip() 再映射。
不要用 dropna() 无差别删除整行:本例中 side 为空的行仍有价格和数量信息,直接删除会损失有效成交。正确做法是分列处理——时间/价量缺失才剔除,方向缺失降级为 0 并记录。