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,供后续分层分析。

逐笔成交数据清洗与联合分布分析表构建 原始逐笔成交表 代码 / 日期 / 时间 成交编号 / 价格 / 数量 买卖方向 / 成交金额 清洗与对齐 ① 时间戳解析与补毫秒 ② 唯一键去重 ③ 单位统一(股→手) ④ 异常分类标记 交易时段划分(session) 开盘集合竞价 连续竞价 收盘集合竞价 停牌日:无记录 → 标记排除,不补0笔 分析表字段:证券代码 | 交易日 | ts | session | vol_lot | price | 方向 | 异常标记 每行 = 一笔成交;笔数 = 行数;每笔成交量 = vol_lot 按日聚合:笔数 N 与平均单笔量 用于联合分布散点 / 二维直方图 分层校验:竞价 vs 连续竞价 避免竞价大单抬高整体分布

五、从逐笔到"每笔成交量与笔数"的聚合

清洗完成后,联合分布通常需要两个层级的表。第一层是逐笔明细表,每行一笔,保留 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 取对数后画直方图,正常应呈明显右偏的单峰形态;若出现孤立尖峰,往往对应单位错误(如把"手"当"股")或未剔除的占位记录。三项通过后,这张表才适合进入联合分布的统计与可视化环节。

常见坑:直接用原始"成交金额/成交数量"反算价格时,若数量单位是手而金额单位是元,会得到100倍偏差的价格,进而误判为异常值。清洗前务必核对单位,而不是先删数据。

把上述步骤固化为脚本后,每次新数据到达只需重跑一遍,并输出剔除日志与三项自检结果。这样得到的分析表口径稳定、可追溯,后续无论做散点、二维直方图还是分位数对比,都不会因底层数据问题而得出错误结论。