4. 批量计算框架选型与对比
逐笔成交数据的特点是“行数极多、单行字段少、计算逻辑相对固定”。一条典型的逐笔记录可能只有证券代码、成交时间、成交价格、成交数量、买卖方向等十来个字段,但一天全市场的数据量可达数千万行甚至上亿行。面对这种数据形态,选型的核心问题不是“哪个框架更先进”,而是“计算密度与数据规模是否匹配框架的调度开销”。本节围绕向量化计算、多进程并行、分布式计算框架(Dask、Spark)三条路线,结合逐笔数据的实际特征给出可操作的决策依据。
三条技术路线的本质差异
向量化计算的本质是把逐行循环改写为整列数组运算。以 Pandas/NumPy 为例,计算每笔成交的成交金额只需 df["price"] * df["volume"],底层由 C 循环完成,避免了 Python 解释器逐行调度的开销。它的优势在于单机内存可容纳数据时性能极高,代码也最简洁;瓶颈在于内存上限和单线程执行——当数据超过内存,或单列计算已经吃满一个核时,向量化本身无法继续加速。
多进程并行是在向量化之上做“横向切分”。典型做法是按证券代码或按时间分片,把数据切成若干块,用 multiprocessing 或 concurrent.futures 分发到多个进程,每个进程内部仍执行向量化逻辑,最后合并结果。它解决的是“单核跑不满”的问题,代价是进程间数据拷贝和结果合并的开销,以及分片边界处需要小心处理的状态(例如按时间切分时,跨片的滚动窗口需要额外对齐)。
分布式计算框架把切分、调度、容错、落盘都交给框架。Dask 的 DataFrame 接口与 Pandas 高度相似,适合在单机多核到小集群之间平滑扩展;Spark 则以 JVM 为基础、以 RDD/DataFrame 为抽象,适合多机、超大数据量、需要与数仓生态打通的场景。两者的共同代价是调度与序列化开销:对于几百万行的小数据,启动一个 Spark 作业的时间可能远超实际计算时间。
逐笔数据下的性能对比要点
把三条路线放到同一份逐笔数据上比较,需要关注四个量:数据规模、单核计算耗时、调度开销、结果合并成本。假设某日单市场逐笔成交约 3000 万行、约 2GB 内存占用,计算 20 个特征(成交金额、买卖方向符号化、按秒聚合的成交量、滚动 20 笔均价等)。在单机 8 核、64GB 内存的机器上,纯向量化实现可能耗时数十秒到数分钟;多进程按证券代码切成 8 片后,理论加速接近线性,但受限于最慢分片和合并开销,实际加速比通常在 4~6 倍;Dask 在单机上与多进程量级相近,但额外承担了任务图构建与调度开销,小数据下反而略慢;Spark 在单机模式下启动与序列化开销明显,只有在数据量达到数十 GB 以上、或需要多机并行时才体现优势。
需要强调的是,上述数字属于教学示例中的合理假设,用于说明量级关系,实际性能必须在本机用真实数据基准测试。一个实用的基准方法是:固定同一份逐笔样本,分别用三种实现跑同一套特征,记录“读入—计算—写出”的端到端时间,并单独记录调度与合并耗时,避免把 I/O 时间误算成计算加速。
选型决策的实操判断
决策可以按以下顺序自问:第一,数据能否装进单机内存?能,优先向量化,把逐行循环全部改写为列运算,这是性价比最高的优化。第二,装不进内存但单机多核可在时限内完成?选多进程,按证券代码分片最自然,因为逐笔特征大多在单只证券内部计算,分片边界清晰;若特征含跨证券的横截面计算,则需在合并阶段补一步全局聚合。第三,单机多核仍不够,且团队已有集群与数仓,选 Spark;若只是单机内存不够、希望保留 Pandas 写法,选 Dask 更轻。
一个可复用的选型对照
| 维度 | 向量化 | 多进程 | Dask | Spark |
|---|---|---|---|---|
| 适用数据规模 | 单机内存内 | 单机内存的数倍 | 单机到小集群 | 数十 GB 以上、多机 |
| 代码改动量 | 最小 | 中,需分片与合并 | 小,接口近似 Pandas | 大,需改写为 DataFrame API |
| 主要开销 | 单核算力 | 进程拷贝与合并 | 任务图调度 | 启动、序列化、Shuffle |
| 容错能力 | 无 | 需自行处理 | 中等 | 强 |
| 典型场景 | 单日单市场特征 | 多日批量回算 | 研究环境扩展 | 生产级全历史流水线 |
落到工程实践,建议采用“先向量化、再并行、最后分布式”的渐进路线:先把特征逻辑写成纯列运算并做单元测试,保证单分片结果正确;再用多进程包装成可分片的批处理函数,验证分片与合并后结果与单机一致;只有当数据规模或时限确实超出单机能力时,才把同一套逻辑迁移到 Dask 或 Spark。这样迁移成本最低,也避免了在小数据上过早引入分布式框架带来的调度负担。