Parquet 批量读取性能分析报告(python)
1. 测试背景
1.1 测试目标
评估多种方案批量读取 Parquet 文件并合并为 pandas DataFrame 的性能差异,找到最优读取方案。
1.2 数据规格
| 项目 | 数值 |
|---|
| 文件数量 | 103 个 parquet 文件 |
| 磁盘占用 | ~483 MB |
| 总行数 | 22,242,728 行 |
| 列数 | 12 列 |
| 数值列 | 8 列 (TradingDay, Open, Close, Settle, PrevClose, PrevSettle, Multiple, TickPrice) |
| 字符串列 | 4 列 (FuturesCode, ContractId, ExchangeContractId, Exchange) |
1.3 测试环境
| 组件 | 版本 |
|---|
| Python | 3.11.6 |
| OS | Windows 11 25H2 |
| CPU | (待补充) |
| RAM | (待补充) |
1.4 测试方法
- 每种方案运行 3 次,取中位数
- 使用
time.perf_counter() 高精度计时 - 每次运行前执行
gc.collect() 减少 GC 干扰
2. 版本对比:pandas 2.3.2 vs 3.0.3
2.1 环境配置
| 组件 | 环境 A | 环境 B |
|---|
| pandas | 2.3.2 | 3.0.3 |
| pyarrow | 23.0.0 | 24.0.0 |
| polars | 1.18.0 | 1.42.1 |
| duckdb | 1.3.2 | 1.5.4 |
2.2 性能对比结果
| 方案 | pandas 2.3.2 | pandas 3.0.3 | 提升倍数 |
|---|
| pa.dataset -> to_pandas() | 4.25s | 0.73s | 5.8x |
| pa.read_table loop -> to_pandas() | 4.58s | 1.20s | 3.8x |
| pd.read_parquet loop + concat | 4.29s | 1.45s | 3.0x |
| strings_to_categorical | 2.35s | 1.83s | 1.3x |
| numeric cols only | 0.59s | 0.37s | 1.6x |
| ArrowDtype wrap only | 0.37s | 0.37s | 1.0x |
| ArrowDtype + full materialize | 6.86s | 6.15s | 1.1x |
| polars -> to_pandas() | 5.89s | 1.26s | 4.7x |
| polars Arrow wrap | 1.38s | 1.25s | 1.1x |
| duckdb -> df() | 10.62s | 12.51s | 0.85x |
2.3 关键发现
pandas 3.0.3 + pyarrow 24 的 to_pandas() 转换有巨大优化:
- 传统全量转换从 4.25s 降至 0.73s,提升 5.8 倍
- 字符串列转换效率大幅提升,可能是优化了 Arrow string -> Python string 的转换路径
- pyarrow 24 本身也有性能改进(polars 读取也同步提升)
3. 方案分类与适用场景
3.1 方案分类
| 类别 | 方案 | 耗时 (pandas 3.0.3) | 输出类型 | 特点 |
|---|
| 惰性包装 | ArrowDtype wrap | 0.37s | Arrow-backed | 零拷贝,按需转换 |
| 列裁剪 | numeric cols only | 0.37s | numpy-backed | 无字符串列,最快 |
| 全量转换 | pa.dataset -> to_pandas() | 0.73s | numpy-backed | 立即可用 |
| 全量转换 | strings_to_categorical | 1.83s | category | 字符串转分类 |
| 惰性+物化 | ArrowDtype + materialize | 6.15s | numpy-backed | 等价于全量 |
3.2 场景推荐
| 场景 | 推荐方案 | 耗时 | 说明 |
|---|
| 全量数据分析 | pa.dataset -> to_pandas() | 0.73s | 立即可用,pandas 3.0.3 推荐 |
| 只需要数值列 | to_table(columns=NUMERIC_COLS) | 0.37s | 列裁剪最快 |
| 延迟按需访问 | to_pandas(types_mapper=pd.ArrowDtype) | 0.37s | 惰性包装 |
| 字符串基数低 | strings_to_categorical=True | 1.83s | 节省内存 |
4. ArrowDtype 惰性物化机制
4.1 原理
to_pandas(types_mapper=pd.ArrowDtype)
↓
为每列创建 ArrowExtensionArray 包装
↓
ArrowExtensionArray._pa_array = 原始 Arrow ChunkedArray 的引用
↓
零拷贝!DataFrame 直接引用 Arrow buffer,无数据复制
↓
调用 .to_numpy() 时才触发真正的 Arrow → numpy 转换
4.2 实测验证
| 操作 | 耗时 | 说明 |
|---|
| to_table() | 0.42s | 读取 parquet 到 Arrow 内存 |
| to_pandas(ArrowDtype) | 0.002s | 仅包装,零拷贝 |
| col.to_numpy() 首次 | 0.08s | 此时才转换 |
| col.to_numpy() 再次 | 0.08s | 每次调用都转换 |
Buffer 地址验证:
- DataFrame 列 buffer:
0x1a4b34d0080 - Table 列 buffer:
0x1a4b34d0080 - 地址相同 = 零拷贝确认
4.3 注意事项
- 转换开销未消失,只是推迟 — 如果最终需要全部列转 numpy,总成本不变
- 真正加速来自少转换 — 列裁剪 + ArrowDtype 组合
- pandas 操作可直接在 Arrow 后端运行 — groupby/filter/sort 无需转 numpy
5. 最佳实践
5.1 推荐代码(pandas 3.0.3+)
import pyarrow.dataset as ds
import pandas as pd
DATA_DIR = "path/to/parquet/files"
dataset = ds.dataset(DATA_DIR, format="parquet")
df = dataset.to_table().to_pandas()
NUMERIC_COLS = ["TradingDay", "Open", "Close", "Settle",
"PrevClose", "PrevSettle", "Multiple", "TickPrice"]
df = dataset.to_table(columns=NUMERIC_COLS).to_pandas()
df = dataset.to_table().to_pandas(types_mapper=pd.ArrowDtype)
5.2 类型兼容性注意
| 场景 | 问题 | 解决方案 |
|---|
| 传入 sklearn/scipy | 不兼容 ArrowDtype | 用 .to_numpy() 转换 |
np.isnan() | 对 string[pyarrow] 报错 | 改用 pd.isna() |
| matplotlib 绘图 | 部分兼容 | 必要时 .to_numpy() |
5.3 版本升级建议
| 当前版本 | 建议 |
|---|
| pandas 2.3.x | 升级到 3.0.3,性能提升显著 |
| pyarrow 23.x | 升级到 24.0.0,配合 pandas 3.0.3 |
| polars 1.18.x | 升级到最新版,to_pandas() 优化明显 |
6. 性能瓶颈分析
6.1 瓶颈分布(pandas 2.3.2)
全量 to_pandas() = 4.25s
├── 数值列转换: ~0.3s (零拷贝,快)
└── 字符串列转换: ~3.9s (创建 8800 万 Python 对象,慢)
└── 占比: 92%
6.2 pandas 3.0.3 优化点
- 字符串转换从 3.9s 降至 ~0.4s
- 可能采用了批量字符串分配或更高效的内存布局
- 具体实现细节需查阅 pandas 3.0 release notes
7. 结论
7.1 最终推荐
| 优先级 | 方案 | 条件 |
|---|
| 1 | pandas 3.0.3 + pyarrow 24 | 全量 to_pandas() 0.73s |
| 2 | 列裁剪 | 只需数值列时 0.37s |
| 3 | ArrowDtype 惰性 | 按需访问场景 |
7.2 关键数字
| 指标 | pandas 2.3.2 | pandas 3.0.3 |
|---|
| 全量可用 DataFrame | 4.25s | 0.73s |
| 只读数值列 | 0.59s | 0.37s |
| ArrowDtype 包装 | 0.37s | 0.37s |
结论:升级到 pandas 3.0.3 + pyarrow 24 是最佳方案,全量读取 2200 万行数据仅需 0.73 秒。
附录:完整测试代码
测试脚本位于:
bench_version_compare.py - 版本对比专用bench_read_parquet_v4.py - 全方案对比test_lazy_materialization.py - 惰性物化验证
数据目录:D:\quantinv_code\...\data\opt_daily_quote