Polars DataFrame 比 Pandas 快的原因
Polars 为什么比 Pandas 更快
Polars 之所以能在多数数据操作场景中显著快于 Pandas,并非只靠单一的魔法优化,而是由底层技术选型、内存模型、执行策略与查询引擎等多个维度共同作用的结果。以下将逐层拆解这些核心原因。
1. 用 Rust 写的引擎,没有 GIL 的束缚
Pandas 重度依赖 NumPy 和 Python 对象层,核心计算虽由 C 语言实现加速,但大量操作仍需回到 Python 解释器执行。Python 的全局解释器锁(GIL)意味着多线程无法真正并行执行 Python 字节码。
Polars 的引擎完全由 Rust 编写。Rust 本身无 GIL,能够安全地跨线程共享数据,从而在读取、过滤、聚合、连接等操作中充分利用现代 CPU 的所有核心。对比之下,Pandas 的并行化通常只能依赖多进程或需要手动管理释放 GIL 的第三方库,使用门槛高且进程间通信开销大。
2. Apache Arrow 作为内存支柱:零拷贝与列式存储
Polars 底层使用 Apache Arrow 作为内存格式,而 Pandas 的内部表示是 NumPy 数组和 Python 对象的混合体。Arrow 带来的优势非常直接:
- 列式存储与内存紧凑:数据按列连续存放,CPU 缓存命中率极高,扫描速度更快。对象类型(如字符串)统一使用高效二进制布局,而不是 Python 对象数组。
- 零拷贝互操作:Arrow 定义了跨语言的标准化内存格式。Polars 之间传递数据,或与 PyArrow、DuckDB 等模块交互时,无需序列化/反序列化,直接共享内存指针。Pandas 的 DataFrame 转成 Arrow 则需要完整的数据拷贝和类型转换。
- 无 Python 对象开销:Polars 的字符串不是 Python str,数值不是 Python int,它们直接以 Arrow 二进制数组存储,操作在 Rust 侧完成,完全绕开 Python 解释器。
3. 主动惰性求值与查询优化
Pandas 默认是立刻执行(eager execution):每一次操作都会立即计算并返回结果,这导致大量中间数据被写回内存,临时 DataFrame 反复创建与销毁。
Polars 提供惰性 API(LazyFrame)。当你调用 .lazy() 后,后续的筛选、聚合、变换等操作只生成一个逻辑执行计划,待到 .collect() 时才真正执行。引擎可以在执行前对整个查询图进行优化:
- 谓词下推(Predicate Pushdown):将过滤条件尽可能推到数据读取阶段,减少读取的数据量。例如
读入 CSV → filter 列 > 100 → select 两列会被优化为扫描时就跳过不符合条件的行和无关列。 - 投影下推(Projection Pushdown):只读取查询用到的列,其余列从 IO 阶段就不再加载。这一优化对于宽表效果极其显著。
- 常量折叠与表达式简化:将冗余计算或可预先求值的表达式在优化阶段消除。
- 切片下推:如果只需要前 N 行,读取阶段就停止,避免全表扫描。
Pandas 用户也可以手动实现一部分上述优化,但需要自行编写大量裁切和分步代码。Polars 的优化器是自动、透明且系统性的。
4. 多核并行无处不在
Polars 的并行不仅体现在单个操作内(即水平并行),还体现在查询执行全程:
- 读取阶段:CSV、Parquet 等文件被自动分块,多线程并行解码。
- 计算阶段:过滤、算术运算、聚合等操作自动利用所有可用线程对列数据进行划分与并行处理。
- 连接操作:高成本的 join 基于 Arrow 的内存布局进行分区、哈希等步骤,全程多线程执行。
- 表达式并行:当某个
select包含多个独立表达式时,它们可以跨列并行求值。
Polars 默认使用所有物理核心,用户无需任何额外设置。而 Pandas 的计算本质上是单线程的,除非配合 Dask、Modin 等外部并行框架。
5. 表达式驱动的算子融合
Polars 设计的核心是**表达式(Expressions)**而不是存储对象。当你书写类似 pl.col("A") + pl.col("B") * 2 的表达式时,引擎会将这一系列操作编译成紧凑的内核。
更重要的是,多个连续的列操作可以被融合成单次遍历。例如:
df.with_columns([
(pl.col("x") * 2 + 1).alias("y"),
pl.col("z").log().alias("log_z")
])
引擎会尽可能通过一次列扫描同时完成 x 的变换和 z 的取对数操作,而不是先扫描一遍计算 y,再扫描一遍计算 log_z。这种融合极大减少了内存带宽压力和遍历开销。在 Pandas 中,每次 df['x']*2 和 np.log(df['z']) 都是独立的列遍历,中间数组会额外分配内存。
6. 真正的流式执行与内存控制
即便在即时执行模式,Polars 的 Eager API 也尽可能以流式方式处理数据,分批加载、分批运算,不会一次性将所有中间结果保存在内存中。在惰性模式下,流式引擎更是允许在全量数据超出内存时依然能够执行——只需配置 collect(streaming=True),优化器就会使用管道化的分批处理算法处理超出 RAM 的数据集。
Pandas 则倾向于一次性将数据全部读入内存,计算产生的临时副本也常驻内存,对于大数据集极易触发 OOM。
7. 高效的字符串与分组聚合实现
Pandas 的 groupby 实现受限于 Python 级别的标签和哈希表操作,字符串处理依赖 Python 对象,性能在基数较高时退化明显。Polars 的分组聚合:
- 基于 Arrow 的字典编码与可字节比较的类型,使用高性能的 Rust 哈希表(如 hashbrown)直接处理二进制数据,不需要生成 Python 对象。
groupby内部对键进行向量化处理,聚合阶段一次性应用所有聚合表达式,避免多次分组遍历。- 字符串处理在 Arrow 二进制数组上原生完成,无需 Python 字符串对象的创建与销毁。
8. 更为现代的 I/O 底层
Polars 的文件读取器也是 Rust 原生实现,通过对数据流的并行解码、缓冲区管理、直接操作 Arrow 阵列等方式,让 CSV、JSON 等非二进制的文本数据解析速度远超 Pandas。Parquet 读取则利用 Arrow 原生的列式推下,实现选择性 I/O。
总结:速度来自“从零开始的权利”
Polars 没有历史包袱,无需迁就 Python 旧生态与 GIL,在设计之初就直接采用 Arrow、Rust、惰性优化、全程并行的技术栈。它把 Pandas 需要进行大量手动优化(且往往还无法实现)的加速点内建到了内核里。对于日常数据工作者来说,使用 Polars 就像自动获得了一位查询优化专家和并行计算高手的协助,这就是它显著快于 Pandas 的根源。