2. 数据获取与清洗实战
联合分布分析的质量,几乎完全取决于底层逐笔数据是否干净、口径是否一致。本节不讨论分布模型,而是聚焦一件更基础的事:把原始逐笔成交表加工成一张"每笔成交量 + 笔数"可直接用于联合分布统计的分析表。我们以常见的逐笔成交字段为例展开,字段包括:证券代码、成交日期、成交时间(精确到毫秒或秒)、成交编号、成交价格、成交数量、买卖方向、成交金额。不同数据源字段名可能不同,但语义基本对应。
一、先明确三个口径,再动手清洗
清洗前必须先固定口径,否则后续所有统计都会漂移。第一是成交量单位:逐笔表里的"成交数量"常见为股,而分析中常需要"手"(1手=100股),必须统一并在字段名中体现,例如 vol_share 与 vol_lot。第二是笔数定义:一笔逐笔成交记录计为1笔,同一毫秒内多笔不合并;若数据源已做撮合合并,需在文档中注明。第三是时间归属:以成交时间落在哪个交易时段为准,用于区分集合竞价与连续竞价。
二、时间戳对齐:把"时刻"变成"可比较的键"
原始时间字段常是 09:30:00.500 这样的字符串,或 93000500 这样的整数。第一步统一解析为带日期的完整时间戳,例如 2024-03-15 09:30:00.500。第二步处理精度不一致:若部分记录只到秒,缺失毫秒按 .000 补齐,而不是丢弃。第三步处理跨日与夜盘:若数据含夜盘,需按交易所规则把夜盘时间归入下一交易日,避免日期错位。第四步检查重复:以"证券代码+成交编号"为唯一键去重,成交编号在同一日内应唯一,重复通常来自多次拉取拼接。
import pandas as pd
df = pd.read_csv("tick_trade.csv", dtype={"证券代码": str})
df["ts"] = pd.to_datetime(
df["成交日期"].astype(str) + " " + df["成交时间"].astype(str),
format="%Y%m%d %H:%M:%S.%f", errors="coerce"
)
df = df.dropna(subset=["ts"])
df = df.drop_duplicates(subset=["证券代码", "成交编号"])
df["vol_share"] = df["成交数量"].astype("int64")
df["vol_lot"] = df["vol_share"] / 100
三、异常值:不是删掉,而是分类处置
逐笔数据中的异常主要有四类,处置方式不同:
- 价格越界:成交价超出当日涨跌停价,多为数据错误,应剔除并记录条数。
- 数量异常:成交数量为0、负数,或明显超过该股流通股本的单笔量,需核查是否为拆单标记或单位错误。
- 时间越界:成交时间落在非交易时段(如 12:00、15:30 之后),需结合交易所时段规则判断是盘后交易还是脏数据。
- 价格跳变:相邻两笔价格偏离超过阈值(如 10%),可能是复权或错行,应标记而非直接删除,供后续复核。
实践中建议保留一份"剔除日志":记录每类异常的条数、涉及证券与日期。这样在联合分布出现异常尖峰时,能快速回溯是市场行为还是清洗副作用。
四、停牌与集合竞价:两类必须单独标记的数据
停牌日的处理原则是"不生成伪笔数"。若某证券某日无任何逐笔记录,不应补0笔,而应标记为停牌并从当日横截面统计中排除;若数据源用一条占位记录表示停牌(成交量为0),需识别并剔除。集合竞价则相反:它真实产生成交,但机制与连续竞价不同。开盘集合竞价(如 09:15–09:25)与收盘集合竞价(如 14:57–15:00)的成交往往集中、单笔量大,若与连续竞价混在一起统计,会显著抬高"大单"比例。建议新增 session 字段,取值如 open_auction、continuous、close_auction,供后续分层分析。
五、从逐笔到"每笔成交量与笔数"的聚合
清洗完成后,联合分布通常需要两个层级的表。第一层是逐笔明细表,每行一笔,保留 vol_lot 与 session,用于刻画单笔成交量的分布形态。第二层是按日聚合表,每个证券每个交易日一行,字段包括:笔数 trade_count(明细行数)、总成交量 vol_sum、平均单笔量 vol_mean、单笔量中位数 vol_median、最大单笔量 vol_max。这张聚合表正是联合分布分析的输入:横轴用笔数,纵轴用平均单笔量或中位数,每个点代表一个"证券—交易日"。
daily = (df.groupby(["证券代码", "交易日"])
.agg(trade_count=("成交编号", "count"),
vol_sum=("vol_lot", "sum"),
vol_mean=("vol_lot", "mean"),
vol_median=("vol_lot", "median"),
vol_max=("vol_lot", "max"))
.reset_index())
六、结果如何判断是否可用
清洗后不要直接进入建模,先做三项自检。其一,笔数与成交量的一致性:vol_sum 应等于明细表按日求和,若不等说明聚合键有误。其二,时段占比合理性:集合竞价笔数占比通常很低(例如不足5%),若占比异常高,可能是时间解析把连续竞价误判为竞价。其三,分布形态检查:对 vol_lot 取对数后画直方图,正常应呈明显右偏的单峰形态;若出现孤立尖峰,往往对应单位错误(如把"手"当"股")或未剔除的占位记录。三项通过后,这张表才适合进入联合分布的统计与可视化环节。
把上述步骤固化为脚本后,每次新数据到达只需重跑一遍,并输出剔除日志与三项自检结果。这样得到的分析表口径稳定、可追溯,后续无论做散点、二维直方图还是分位数对比,都不会因底层数据问题而得出错误结论。