3 个 Polars 高性能数据处理技巧

💡 原文英文,约1000词,阅读约需4分钟。
📝

内容提要

Polars 提速的核心是让计算留在引擎内。三个技巧:用 scan_parquet 替代 read_parquet,让优化器下推过滤和列裁剪,避免中途 collect;用 over() 在单次遍历中完成组内聚合,替代 group_by 加 join;用 when/then/otherwise 替代 map_elements,避免逐值调用 Python。

🔎

延伸解读

惰性求值与查询优化:让计算留在引擎内

文章强调 Polars 的速度来自 Rust 表达式引擎和查询优化器。使用 scan_parquet 返回 LazyFrame,优化器能将过滤和列裁剪下推到扫描阶段,避免读取和计算被丢弃的数据。而 read_parquet 会先加载全量数据再过滤,造成浪费。关键习惯是避免在链式调用中途 collect(),因为每次 collect() 都会阻断优化器。

over() 替代 group_by+join:单次遍历完成组内聚合

当需要将组级聚合值广播回每一行时,传统做法是 group_by 加 agg 再 join,这需要两次遍历和物化中间结果。over() 在单个表达式中完成,保持行顺序,默认 group_to_rows 映射策略将聚合值映射回原行。它还支持 order_by,便于计算累计值或组内滞后。仅在需要改变数据形状时才用 explode。

避免 map_elements:用原生表达式替代逐值 Python 调用

map_elements 会逐值调用 Python 函数,文档明确指出其比原生表达式 API 慢得多,Polars 甚至会发出 PolarsInefficientMapWarning。大多数场景可用 when/then/otherwise 原生处理条件逻辑。注意:Polars 会并行计算 when/then 链的每个分支再过滤,因此每个分支必须独立有效。

Q&A

Polars 为什么比 Pandas 快?

Polars 的速度来自两个方面:一是用 Rust 编写并在所有核心上执行的表达式引擎,二是查询优化器会在任何代码运行前重写你的工作。几乎每个慢的 Polars 脚本都缺少这两者之一。

Polars 中 scan_parquet 和 read_parquet 有什么区别?

read_parquet 会把整个文件内容拉入内存然后再过滤;scan_parquet 返回 LazyFrame,只记录请求而不立即执行。优化器会把过滤和列裁剪下推到扫描阶段,使数据在读取时就被缩小,被丢弃的行永远不会被解码,从而避免浪费计算。

在 Polars 中如何避免中途调用 collect()?

不要因为担心而在链式操作中途调用 collect()。每次 collect() 都是优化器无法看穿的墙。应该保持 LazyFrame 直到最后再调用一次 collect(),这样优化器才能进行下推等优化。

Polars 的 over() 方法有什么作用?

over() 可以在单个表达式中、一次遍历内完成组内聚合,并保持行顺序。它替代了 group_by 加 agg 再加 join 回原表的两遍操作,避免了物化中间结果和连接键的麻烦。默认映射策略 group_to_rows 会把聚合值映射回原来的行。

Polars 中 map_elements 为什么慢?

map_elements 会把列中的每个值逐个交给 Python 可调用对象,文档明确指出它“比原生表达式 API 慢得多”。Polars 甚至会在发现可替换的 map 时抛出 PolarsInefficientMapWarning。大多数使用场景是条件判断,可以用 when/then/otherwise 原生处理。

Polars 中如何用 when/then/otherwise 替代 map_elements?

可以用 pl.when(条件).then(值).when(条件).then(值).otherwise(值) 来构建条件分支,完全避免 Python 调用。例如对 fare_amount 分箱:大于50为high,大于20为medium,否则为low。注意 Polars 会并行计算每个分支再过滤,因此每个分支必须独立有效。

Polars 脚本变慢时应该首先检查什么?

首先问自己越过了哪条边界:是把工作交还给了 Python 还是内存。快速版本让计算留在引擎内,慢速版本则把工作交给 Python 或内存。记住:用 scan 代替 read,用表达式代替循环。

🏷️

标签

➡️

继续阅读