1. 逐笔CSV数据特征与读取瓶颈诊断

逐笔成交数据是量化研究中粒度最细的行情数据之一。与日线、分钟线不同,它记录的是每一笔撮合成交的明细,通常包含成交时间、证券代码、成交价格、成交数量、买卖方向、成交编号等字段。沪深两市的逐笔成交每天可达数千万笔,单只活跃股票一天就可能产生数十万条记录。当研究标的扩展到全市场、时间跨度拉长到数月甚至数年时,数据会以“按日分文件、按市场分目录”的形式组织,形成成千上万个 CSV 文件,总体积轻松达到几十 GB 甚至上百 GB。

这种数据形态决定了读取环节的三个典型压力点。第一是文件数量多:每个交易日、每个市场、甚至每只股票一个文件,打开和关闭文件的系统调用开销会累积成不可忽视的成本。第二是单文件体积大:一个交易日的全市场逐笔文件可能达到数 GB,一次性读入内存容易触发内存峰值。第三是字段解析重:时间戳、价格、数量等字段需要类型转换,字符串到数值的解析在千万行级别上会占据大量 CPU 时间。因此,在动手优化之前,必须先搞清楚时间到底花在哪里——是磁盘 I/O、CSV 解析,还是内存分配与垃圾回收。

逐笔CSV读取链路与瓶颈定位 数据源 按日/按市场分文件 单日全市场数 GB 文件数可达上万 读取阶段 open / read 系统调用 磁盘 I/O 与页缓存 逐行迭代或分块 解析阶段 分隔符切分 字符串→数值类型转换 时间戳解析 内存阶段 DataFrame 构建 峰值内存 GC 压力 瓶颈①:小文件过多 系统调用与元数据开销 瓶颈②:逐行解析 Python 循环 + 类型转换 瓶颈③:内存峰值 全量载入易 OOM 热点定位工具与观察指标 cProfile(函数级) cumtime:累计耗时 tottime:函数自身耗时 ncalls:调用次数 line_profiler(行级) @profile 装饰目标函数 kernprof -l -v 运行 % Time / Hits 定位热点行 结果判读 I/O 占比高 → 合并/分块 解析占比高 → 向量化/指定 dtype 内存占比高 → 分块/列裁剪

上图把读取链路拆成“数据源—读取—解析—内存”四段,并标出三个常见瓶颈与对应的诊断工具。实际排查时,建议先用 cProfile 做函数级扫描,确认耗时集中在哪个函数;再对可疑函数用 line_profiler 做行级下钻,看具体是哪一行在“吃时间”。

下面给出一个可直接运行的诊断示例。假设数据目录 ./tick/20240102/ 下有若干逐笔 CSV,字段为 time,code,price,volume,direction。先用 cProfile 包住读取函数:

import cProfile, pstats, io
import pandas as pd
from pathlib import Path

def load_one(path):
    df = pd.read_csv(path, encoding="utf-8")
    df["time"] = pd.to_datetime(df["time"], format="%H:%M:%S")
    df["price"] = df["price"].astype("float64")
    df["volume"] = df["volume"].astype("int64")
    return df

def load_all(folder):
    frames = []
    for p in Path(folder).glob("*.csv"):
        frames.append(load_one(p))
    return pd.concat(frames, ignore_index=True)

pr = cProfile.Profile()
pr.enable()
load_all("./tick/20240102/")
pr.disable()

s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats("cumtime").print_stats(15)
print(s.getvalue())

观察输出时重点看三列:ncalls 反映调用频次,tottime 是函数自身耗时,cumtime 是含子调用的累计耗时。如果 read_csv 的 cumtime 占了大头,说明瓶颈在解析与 I/O;如果 to_datetime 的 tottime 异常高,说明时间戳解析是热点;如果 concat 耗时突出,则要警惕内存拼接带来的复制开销。需要注意,cProfile 本身有开销,适合定位相对占比,不适合当作绝对性能基准。

确认了可疑函数后,用 line_profiler 做行级分析。给 load_one 加上 @profile 装饰器,然后执行 kernprof -l -v diagnose.py,输出会逐行给出 Hits、Time 和 % Time。例如可能看到 pd.to_datetime 那一行占了 60% 以上时间,这就直接指明了优化方向:改用 format 参数固定格式、或先以字符串读入再统一转换,甚至改用 engine="pyarrow" 加速解析。

诊断阶段先不要急着改代码。把“读取、解析、类型转换、拼接”分别计时,形成一份基线数据(例如总耗时 42 秒,其中解析 25 秒、类型转换 9 秒、拼接 6 秒),后续每一步优化都用同一份数据复测,才能判断优化是否真的有效。