1. 课程导入

在金融数据工程中,我们面对的不是“几张表、几百万行”的常规业务数据,而是行情快照、逐笔成交、委托队列、因子矩阵、风险敞口、回测中间结果等高频、宽表、多版本、强时序的数据集合。一个中型量化研究平台每天新增的原始行情可能达到数十GB,因子计算过程中还会产生数倍于原始数据的中间列。此时,传统基于行式存储与逐行对象处理的技术栈会迅速暴露瓶颈:磁盘I/O放大、内存占用失控、CPU缓存命中率低、跨语言传输开销高。本课程的第一节不急于写代码,而是先建立一张“技术地图”:金融数据工程到底难在哪里,列式存储与内存计算为什么能解决问题,Parquet与Arrow各自扮演什么角色,以及我们接下来的学习路径如何与实战目标对齐。

一、金融数据工程的三类核心挑战

金融数据工程与通用大数据处理的最大区别在于“时间、精度、宽度”三个维度同时施压。时间维度上,行情数据具有严格的时序性和事件驱动特征,乱序、重复、缺失、交易所时钟差异都会影响后续计算;精度维度上,价格、收益率、持仓、保证金等字段对浮点误差和舍入方式极其敏感;宽度维度上,因子研究和风险模型往往需要同时加载数百甚至数千列,而每次计算只涉及其中少数列。这三类挑战直接决定了存储格式与内存模型的选择。

  • I/O放大:行式存储按整行读取,计算单列因子时不得不把无关列全部读入,造成大量无效I/O。
  • 内存膨胀:Python对象逐行存储的元数据开销远大于数据本身,百万行数据可能占用数GB内存。
  • 序列化开销:跨进程、跨语言传递数据时,反复序列化与反序列化会吃掉大量CPU时间。
  • 版本与回溯:回测需要可复现的数据快照,格式必须支持Schema演进与分区裁剪。
金融数据工程的本质矛盾:数据越来越宽、越来越快,但计算往往只关心少数列和特定时间窗口。解决方向不是“更快的硬盘”,而是“让存储和内存都按列组织”。

二、列式存储与内存计算的价值

列式存储的核心思想是:同一列的数据在物理上连续存放。这样带来三个直接收益。第一,按需读取:查询只涉及某几列时,只需读取对应列的数据块,I/O量大幅下降。第二,高压缩比:同一列数据类型一致、取值分布集中,字典编码、游程编码、Delta编码等更容易生效。第三,向量化计算:连续内存布局让CPU可以批量处理数据,减少分支预测失败和缓存未命中。

内存计算则进一步把“列式”从磁盘延伸到内存。Arrow定义了一套语言无关的列式内存格式,使Python、Java、C++、Rust等不同运行时可以共享同一块内存,而不需要复制或转换。对于金融场景,这意味着行情数据从C++撮合引擎写出后,Python研究环境可以直接零拷贝读取;因子计算结果可以交给Rust做高性能聚合,再返回给Notebook可视化。列式存储解决“怎么存”,内存计算解决“怎么算”,两者结合才构成完整的数据工程链路。

金融数据工程链路:存储层 → 内存层 → 计算层 Parquet(磁盘列式) 行情快照 / 逐笔成交 因子矩阵 / 风险敞口 列裁剪 + 分区裁剪 字典 / Delta / 游程编码 Schema演进与快照 解决“怎么存、存得省” Arrow(内存列式) 零拷贝跨语言共享 连续内存 + 向量化 Python / C++ / Rust RecordBatch / Table Flight / IPC 传输 解决“怎么算、传得快” 计算与实战 因子计算 / 回测 风险聚合 / 实时监控 Pandas / Polars / DuckDB PyArrow Compute 流式与批处理 解决“算得准、跑得快” 读取 计算

三、Parquet与Arrow的定位差异

很多初学者会把Parquet和Arrow混为一谈,认为“都是列式,选一个就行”。实际上它们位于数据生命周期的不同阶段,解决的问题也不同。Parquet是磁盘上的列式文件格式,强调压缩、编码、分区和长期存储;Arrow是内存中的列式数据格式,强调零拷贝、跨语言和向量化计算。二者不是竞争关系,而是上下游协作关系:Parquet负责把数据高效地放在磁盘上,Arrow负责把数据高效地放在内存中并交给计算引擎。

对比维度 Parquet Arrow
主要定位 磁盘列式存储格式 内存列式数据格式
核心优势 高压缩比、列裁剪、分区裁剪、Schema演进 零拷贝、跨语言、向量化、低序列化开销
典型使用场景 行情归档、因子快照、回测数据湖 Notebook分析、跨进程传输、实时计算
与计算引擎关系 作为数据源被读取 作为内存载体被直接计算
常见误区 认为Parquet可以直接原地计算 认为Arrow可以替代持久化存储

四、学习路径与实战目标

本课程围绕“存得省、读得快、算得准”三条主线展开,共30章。前段建立Parquet与Arrow的核心心智模型,中段进入金融场景的实战:行情数据清洗、因子矩阵构建、回测数据快照、跨语言传输与性能调优,后段覆盖分区策略、压缩参数、内存布局、流式处理与常见故障排查。每一章都尽量给出可运行的代码片段和可观测的性能指标,而不是停留在概念介绍。

  1. 基础认知:理解列式存储、内存计算、Parquet与Arrow的定位与协作关系。
  2. 读写实战:使用PyArrow读写Parquet,掌握Schema定义、分区写入与列裁剪。
  3. 金融场景:处理行情快照、逐笔成交、因子矩阵与回测快照。
  4. 性能调优:压缩编码、Row Group大小、内存池、零拷贝与向量化。
  5. 工程化:数据版本、Schema演进、跨语言服务与故障排查。
学习建议:每章至少准备一份真实或模拟的金融数据集,边学边用。Parquet与Arrow的很多参数只有在实际数据上才能看出差异,例如Row Group大小对扫描性能的影响、字典编码对低基数列的压缩效果。

五、常见误区与复盘

在正式进入后续章节之前,先澄清几个高频误区。第一,“列式存储一定比行式快”:如果查询总是需要整行数据,列式反而可能因为多次寻址而变慢。第二,“Parquet文件越小越好”:过度压缩会增加CPU解压开销,金融低延迟场景需要权衡压缩率与解压速度。第三,“Arrow就是Pandas”:Pandas底层可以使用Arrow,但Pandas的索引、对象类型和Arrow的列式模型并不完全等价。第四,“零拷贝等于没有成本”:零拷贝依赖内存对齐和生命周期管理,跨进程共享时仍需注意内存所有权和释放时机。

# 一个典型的金融数据读取对比(示意)
# 行式CSV:读取全部列,再筛选
import pandas as pd
df = pd.read_csv("tick.csv")          # 所有列进入内存
df = df[df["symbol"] == "IF2406"]     # 过滤发生在读取之后

# Parquet + Arrow:先列裁剪,再过滤
import pyarrow.parquet as pq
table = pq.read_table(
    "tick.parquet",
    columns=["symbol", "price", "volume"],  # 只读需要的列
    filters=[("symbol", "=", "IF2406")]     # 分区/行组裁剪
)
金融数据对精度和时序极其敏感。使用Parquet与Arrow时,务必明确时间戳单位(毫秒/微秒/纳秒)、时区、价格字段的Decimal或浮点类型,以及空值语义。很多“计算结果对不上”的问题,根源不在算法,而在数据格式与类型定义。

本章的目标不是让你立刻成为Parquet与Arrow专家,而是先建立一张清晰的技术地图:金融数据工程的挑战来自时间、精度与宽度;列式存储与内存计算分别解决“怎么存”和“怎么算”;Parquet与Arrow是上下游协作关系,而非二选一。接下来,我们将从Parquet的文件结构开始,逐步进入可运行的实战环节。