热门搜索:和平精英 原神 街篮2 

您的位置:首页 > > 教程攻略 > ai教程 >Polars vs Pandas 在生产 Pipeline 中的对比

Polars vs Pandas 在生产 Pipeline 中的对比

来源:互联网 更新时间:2026-06-30 07:37

大多数 Python 数据工程师最早接触的库就是 pandas。说它是行业标准一点也不为过,能用,而且一直够用,所以很少有人会去质疑它。

但 pandas 诞生于 2008 年,设计思路是针对那个时代的数据问题:假设每个操作都要立即返回结果,假设单个 CPU 核心就足够,假设数据能塞进内存。这些假设在很多年里都成立。可随着数据 pipeline 规模的增长,它们越来越站不住脚了。
\
这里不是要否定 pandas——它很好,我现在也还在用。真正的问题是:它是否还匹配你实际在跑的工作负载?

对于超过 1 GB 左右的文件型 ETL 来说,并不是 pandas 本身不行,而是这个生态里已经出现了一个结构上更合适的工具——Polars。两者之间的差距可不仅仅是量级上的小打小闹。

Polars 为何在结构上更快

在聊基准测试数据之前,先搞清楚原理。下图并排展示了两种执行模型。
\
Pandas 的单线程即时执行(eager execution) vs. Polars 的多线程惰性执行(lazy execution)。

Pandas 的执行模型


Pandas 运行在 NumPy 之上,大多数操作是单线程的。Python 的全局解释器锁(GIL)阻止了并行执行,即便有多个线程可用也不行。每个操作都立即执行,所以无论是否需要,中间结果都会被创建并存入内存。对一个 5 GB 的 DataFrame 做五步变换,就要产生五份中间副本,内存占用迅速叠加。

另外,pandas 默认把字符串存储为 NumPy object 数组:每个值都是一个 Python 对象指针,每个唯一字符串大约占 67 字节,而列式存储只需要 4 字节的额外开销。100 万个 18 字符的字符串,在 pandas object dtype 下约占 75 MB,同样的列在 Arrow 格式下只需约 22 MB。

Polars 的执行模型


Polars 用 Rust 编写,完全运行在 GIL 之外。数据以 Apache Arrow 列式格式存储:每列是一块连续的类型化内存,对 CPU 缓存友好,也支持 SIMD 指令。调用 scan_parquet 时,不会读取任何数据。Polars 先构建逻辑执行计划,在触碰任何字节之前完成优化:谓词下推到扫描层、未使用的列在读取前裁剪、Join 顺序重排以提升效率。

执行采用 morsel 驱动模式。每个 CPU 线程取一块输入,用自己独立的本地状态处理,最后合并结果,计算过程中没有锁竞争,所有核心同时运行。

处理一个 10 GB Parquet 文件时,pandas 读取全部 10 GB,在单核上顺序处理,并在途中产生中间副本。同样的 Pipeline 在 Polars 中,只读取满足过滤和聚合所需的列和行组,在所有核心上并行处理,不会实体化任何不必要的中间结果。每次给 Pipeline 计时,这种差距都能感受到:pandas 还在读取的时候,Polars 已经在规划了,但 Polars 总是先完成。

基准测试

下表汇总了不同规模和负载类型下的结果。
\
三个独立基准测试,分别对比了查询 Pipeline、生产 ETL 迁移和调度负载场景下 Polars 与 pandas 的表现。

PDS-H 基准测试


Polars 团队于 2025 年 5 月发布了更新的 PDS-H 测试结果,运行环境为 AWS c7a.24xlarge 实例(96 vCPU、192 GB RAM)。测试对 10 GB CSV 数据集运行全部 22 条 TPC-H 衍生查询。Polars 流式处理耗时 3.89 秒,pandas 2.2.3(启用 PyArrow dtype)耗时 365.71 秒——整条多步分析 Pipeline 下来,差距达到了 94 倍。

在 Scale Factor 100(100 GB)的场景下,pandas 直接出局了。单线程执行加上缺乏查询优化器,导致在完成基准测试之前就触发了内存溢出(OOM)。Polars 流式处理耗时 23.94 秒。

当然,这是厂商自己跑的基准测试,可以把它当作方向性参考。在普通硬件上的独立测试差距通常小一些,单个操作一般在 5 到 22 倍之间。那个标题级别的倍数是查询优化器和多线程在完整 Pipeline 中叠加后才出现的。

生产环境的实证


荷兰出行服务商 Check Technologies 在一个 Sprint 内把全部 100 多个 Airflow DAG 从 pandas 迁移到了 Polars。驱动力不是性能基准,而是最数据密集的 Pipeline 上反复出现的 OOM 错误。迁移耗时不到两周:最严重的 DAG 速度提升了 3.3 倍,几乎所有其他 DAG 提升约 2 倍,云基础设施成本降低了 25%。他们的高级数据工程师 Paul Duvenage 表示,团队一旦切换到声明式表达式 API,迁移过程就非常顺畅。

DB Systel(德国铁路子公司)用 Polars 0.20 重写了一个列车调度重处理任务。该任务原本需要 96 分钟,Polars 版本只需 5.5 分钟——在真实生产负载上提升了 17.5 倍,而不是在合成基准上。

在小数据上,差距基本消失,有时甚至会反转。Polars 的查询优化器存在规划开销,当数据集小于几百 MB 时,这个开销会超过计算节省。对于小 DataFrame 上的快速脚本,pandas 写起来更快,跑起来也够快。

安装与运行

安装只需一条命令:

# 创建项目并添加 Polars
uv init polars-pipeline
cd polars-pipeline
uv add polars pyarrow
# 标准安装
uv run pipeline.py

无需编译,无需系统依赖。Polars 以预编译 wheel 形式发布,Rust 运行时已打包在内。

开始之前有一点值得注意:在 Apple Silicon(M 系列 Mac)上,标准 polars wheel 会触发 CPU 兼容性警告,并建议使用运行时兼容版本。

# Apple Silicon:改用这个
uv add "polars[rtcompat]" pyarrow

Linux 和 Intel Mac 上使用标准安装即可。在 M 系列硬件上,polars[rtcompat] 可避免警告,并确保使用适合该处理器的正确 SIMD 指令。

并排对比:两个库实现相同操作


第一次看 Polars 代码时,语法感觉很陌生。表达式 API 需要一天时间适应,适应之后,会发现它比 pandas 的等价写法更易读,而不是更难读。下面的代码执行完全相同的 ETL 变换:读取 Parquet 文件、过滤行、按类别聚合、排序结果。先是 pandas 版本,然后是 Polars LazyFrame 版本。

# pandas 版本
import pandas as pd
import time
start = time.perf_counter()
df = pd.read_parquet("sales.parquet")
result = (df[df["revenue"] > 1000]
          .groupby("category")
          .agg(total_revenue=("revenue", "sum"),
               a vg_price=("price", "mean"),
               order_count=("order_id", "count"))
          .sort_values("total_revenue", ascending=False)
          .reset_index())
print(f"pandas: {time.perf_counter() - start:.3f}s")
print(result)
# Polars 版本——带谓词下推和投影下推的惰性执行
import polars as pl
import time
start = time.perf_counter()
result = (pl.scan_parquet("sales.parquet")  # 此时不读取任何数据
          .filter(pl.col("revenue") > 1000) # 下推到扫描层
          .group_by("category")
          .agg(total_revenue=pl.col("revenue").sum(),
               a vg_price=pl.col("price").mean(),
               order_count=pl.col("order_id").count())
          .sort("total_revenue", descending=True)
          .collect()    # 执行在这里发生
)
print(f"Polars: {time.perf_counter() - start:.3f}s")
print(result)

结构上的关键差异在于 scan_parquet 对比 read_parquet。Pandas 版本立即读取整个文件;Polars 版本构建执行计划,只读取满足过滤和聚合所需的行和列。对于一个 1 GB、20 列、实际只需要 3 列的文件,Polars 读取的字节数可能不到 pandas 的 20%。

流式处理超出内存容量的数据


当数据集超过可用内存时,在 collect 中加入 engine="streaming"。Polars 以称为 morsel 的批次处理数据,自适应地溢出到磁盘,始终不将完整数据集保留在内存中。

result = (pl.scan_parquet("large_dataset/*.parquet")
          .filter(pl.col("status") == "active")
          .group_by("region")
          .agg(pl.col("amount").sum())
          .sort("amount", descending=True)
          .collect(engine="streaming")  # 核外执行
)

如需直接写入磁盘而完全不在内存中实体化,使用 sink_parquet

(pl.scan_parquet("raw/*.parquet")
 .filter(pl.col("error_code").is_null())
 .sink_parquet("clean/output.parquet")  # 分批流式写入磁盘
)

生产环境注意事项


在生产环境中显式设置线程数,以避免与其他进程争抢资源:

import os
os.environ["POLARS_MAX_THREADS"] = "8"  # 必须在导入 polars 之前设置
import polars as pl
# Polars 在导入时初始化线程池——
# 导入后再设置此变量无效

pyproject.toml 中锁定 Polars 版本。1.x API 已稳定,但小版本更新会引入新特性,在边缘情况下可能改变行为。锁定版本,在与生产 OS 一致的容器中测试,有意识地升级。

Pandas 仍然占优的场景

Polars 是 1 GB 以上文件型 ETL 的更好选择,下图按数据规模和负载类型梳理了决策框架。
\
按数据规模和负载类型选择工具,并明确标注了机器学习生态系统的约束。

大多数机器学习库以 pandas DataFrame 作为原生输入格式。scikit-learn、statsmodels 以及很多绘图库,要么明确要求 pandas,要么默认如此。为了省一次 .to_pandas() 调用而重写整个技术栈,这笔买卖不值得。

在每一个需要给模型喂数据的 Pipeline 中,实践中的做法是混合方案:用 Polars 做繁重的计算工作,最后一步再转换:

# 用 Polars 做重活
features = (pl.scan_parquet("events/*.parquet")
            .group_by("user_id")
            .agg([pl.col("session_duration").mean().alias("a vg_session"),
                  pl.col("purchase").sum().alias("total_purchases"),
                  pl.col("event_date").max().alias("last_seen")])
            .collect(engine="streaming"))
# 在边界处零拷贝转换
import sklearn
X = features.to_pandas()  # 基于 Arrow,几乎瞬间完成

转换几乎瞬间完成,因为 Polars 和新版 pandas 共享 Apache Arrow 内存格式——当两边都使用 Arrow 类型时,不会发生数据拷贝。

Pandas 3.0 带来了什么变化


2026 年 1 月发布的 pandas 3.0,将 PyArrow 支持的字符串设为字符串列的默认类型,并将写时复制(Copy-on-Write)设为唯一执行模式。这些改动使字符串密集型数据集的内存占用降低了最多 70%,并消除了一类静默的变异 bug。

这是真实的改进。它在 I/O 和内存方面实质性地缩小了差距。但多核执行的差距依然存在——pandas 的聚合操作在任何版本中仍然是单线程、即时执行的,也没有查询优化器。如果瓶颈是计算而非内存,3.0 版本并没有改变这一判断。

总结

有三个信号说明一条 Pipeline 已经到了该迁移的时候:

第一:在生产规模的数据上触发 OOM,或者已经为了绕过内存限制加入了分块逻辑。第二:本该几秒完成的任务跑了好几分钟,性能分析显示 pandas 的 groupby 或 join 操作占据了主要运行时间。第三:运行在一台多核机器上,执行期间那些 CPU 核心基本闲着。

迁移的时候可以选最慢的那条 Pipeline,或者最常 OOM 的那条。只用 Polars LazyFrame 重写计算密集的部分,在输出端保留 pandas 边界以衔接机器学习或绘图,然后在生产规模的数据上基准测试,再推上线。如果在超过 1 GB 的数据集上看不到至少 3 倍的提升,瓶颈可能根本不在 DataFrame 库上。

Pandas 不是遗留技术,它是精准的专用工具;Polars 是另一种工作的精准专用工具。为每项工作选择合适的,这不是迁移项目,而是工程判断力。

热门手游

手机号码测吉凶
本站所有软件,都由网友上传,如有侵犯你的版权,请发邮件haolingcc@hotmail.com 联系删除。 版权所有 Copyright@2012-2013 haoling.cc