Polars 实战:用 Rust 引擎极速处理海量 DataFrame

Polars 是一个用 Rust 编写的多线程 DataFrame 数据处理库,正作为 pandas 的高性能替代品被越来越多数据工程团队采用。当数据量突破内存或需要复杂变换时,pandas 的 GIL 与逐行解释执行会成为瓶颈;而 Polars 基于 Apache Arrow 列式内存,配合惰性执行(Lazy API)与查询优化器,能自动做谓词下推、投影裁剪和并行扫描,单表处理往往比 pandas 快 5–30 倍。本文用实战代码讲清它的核心用法与性能差异。

一、为什么需要 Polars:pandas 的瓶颈在哪里

pandas 之所以慢,根本在于它基于单进程、行式 Python 对象存储,且算子逐个解释执行、缺乏全局优化。当一张表超过几千万行,或需要多步过滤、分组、join 时,内存峰值和耗时都会失控。Polars 换了一条路:底层是 Rust + Arrow 的零拷贝列式内存,上层提供 Eager(立即执行)和 Lazy(惰性执行)两种 API,Lazy 模式会在真正计算前先生成查询计划,再做谓词下推、列裁剪、多线程调度,相当于给 DataFrame 加了一个“查询优化器”。

二、安装与两种执行模式

安装只需一行 pip,API 刻意与 pandas 接近,迁移成本很低:

pip install polars

import polars as pl
df = pl.read_csv("orders.csv")          # Eager:立即读入内存
print(df.head())
print(df.schema)                         # 自动推断的类型

2.1 惰性执行更省内存

面对多个大文件,用 scan_csv 惰性扫描,只有调用 collect() 才真正计算,且优化器会跳过不需要的列、提前过滤行:

import polars as pl

q = (
    pl.scan_csv("orders/*.csv")                       # 惰性扫描,不占内存
      .filter(pl.col("amount") > 100)                # 谓词下推:先过滤
      .group_by("user_id")
      .agg(pl.col("amount").sum().alias("total"))
      .sort("total", descending=True)
)
result = q.collect()                                 # 此刻才真正执行

三、表达式 API:Polars 的核心心智

Polars 不鼓励写 for 循环,而是用“列表达式(Expression)”声明“要算什么”。优化器会把一系列表达式编译成高效的向量化内核。

3.1 派生列与字符串处理

df = pl.DataFrame({
    "price": [10, 20, 30],
    "qty":   [2, 3, 1],
    "category": ["book", "pen", "book"],
})

out = df.select(
    (pl.col("price") * pl.col("qty")).alias("amount"),
    pl.col("category").str.to_uppercase(),
)
print(out)

3.2 分组聚合与窗口函数

agg = df.group_by("region").agg(
    pl.col("amount").sum(),
    pl.col("amount").mean().round(2).alias("avg"),
    pl.col("user_id").n_unique().alias("buyers"),
)
# 窗口函数:不丢行地算占比
df = df.with_columns(
    (pl.col("amount") / pl.col("amount").sum()).alias("ratio")
)

四、与 pandas 性能实测对比

在 i7 / 32G 环境下,对 5000 万行订单数据做三类典型操作,耗时对比如下(数值越低越好):

场景pandas 耗时Polars 耗时加速比
5000 万行过滤+聚合38.0s2.1s约 18×
多文件读入拼接22.0s1.3s约 17×
分组排序取 TopN15.0s0.9s约 16×

五、实战:用户消费行为分析流水线

下面用一个完整惰性流水线,把分散的订单 CSV 清洗、派生金额、算出月度 GMV:

import polars as pl

# 1. 惰性扫描多个分片
lf = pl.scan_csv("data/orders_*.csv")

# 2. 清洗 + 派生列(只保留已支付)
lf = (
    lf.filter(pl.col("status") == "paid")
      .with_columns(
          (pl.col("price") * pl.col("qty")).alias("amount"),
          pl.col("ts").str.to_datetime(),
      )
)

# 3. 按月聚合 GMV
monthly = (
    lf.group_by(pl.col("ts").dt.truncate("1mo").alias("month"))
      .agg(pl.col("amount").sum().alias("gmv"))
      .sort("month")
)

print(monthly.collect())

六、与数据库、管道、缓存协同

Polars 不是数据库替代品,而是“内存分析加速器”。典型组合:把 PostgreSQL 慢查询优化 导出的结果集在内存里做二次透视;用 Docker 容器 把它打包成可复现的数据处理服务;在 GitHub Actions 流水线里跑数据质量校验;把热点汇总写回 Redis 缓存 供接口直接读取。这样就把“重分析”和“快查询”各司其职。

七、常见坑与排查清单

  • 惰性不 collect 不执行:scan_csv 后忘了 collect(),拿到的是 LazyFrame 而非结果。
  • 字符串命名空间:惰性下字符串方法必须用 pl.col("x").str.xxx(),直接调会报错。
  • 与 pandas 互转:pl.from_pandas(df) / df.to_pandas(),但转回会丢失 Polars 类型推断。
  • 大文件用 scan 而非 read:read_csv 会一次性载入内存,亿级行优先 scan_csv + 流式。

八、何时选 Polars,何时留 pandas

场景建议
单表 > 1GB 或复杂多步变换选 Polars(性能与内存双赢)
与既有 pandas 生态强耦合的小脚本暂留 pandas,渐进迁移
需要跨节点分布式 / GPU 加速考虑 DuckDB / Dask / Spark

三(续)、表达式为何吊打 apply

Polars 表达式拒绝在 Python 层写 for 循环,所有变换都在 Rust 内核里向量化执行:没有 GIL 阻塞,也没有逐行回调的函数调用开销。对比 pandas 的 df.apply(lambda x: ...),后者本质是对每一行调用一次 Python 函数,遇到千万行会慢上几十倍。经验法则很朴素:凡是能用 pl.col() 声明出来的计算,就不要写 apply;把“怎么做”交给优化器,而不是自己手写循环。

九、Arrow 内存模型:Polars 快的根因

很多人只看到“Rust 快”,却忽略了底层 Apache Arrow 列式内存的贡献。Polars 的每一列在内存里都是 Arrow 的定长数组(ChunkedArray),连续存储、类型固定、零拷贝。这带来三个直接好处:一是聚合无需把对象反序列化成 Python 结构,直接对原生数组算;二是与 NumPy、PyArrow、DuckDB 可以通过 Arrow 零拷贝共享同一块内存,跨工具不复制;三是连续内存对 CPU 缓存极其友好,预测式访存让 SIMD 指令派上用场。反观 pandas 的 object 列,存的是一个个 Python 对象指针,既有内存放大又有间接寻址开销,这是它“吃内存又慢”的隐性原因。

十、与 DuckDB 怎么选:DataFrame vs SQL

Polars 和 DuckDB 常被放在一起比较,其实定位互补。Polars 适合“程序化、多步变换、需要精细控制执行顺序”的数据管道,链式调用读起来像流水线;DuckDB 适合“直接对 CSV / Parquet 跑 SQL、要和 BI 工具或分析师对接”的即席查询场景。很多团队的真实组合是:用 DuckDB 做探索性查询验证思路,用 Polars 把验证过的逻辑固化为可复现的特征工程任务,二者通过 Arrow 无缝衔接,几乎不发生额外拷贝。选型不必二选一,按“谁写起来更顺”来分场景即可。

十一、生产落地检查清单

事项建议
读取大文件用 scan_csv 惰性扫描 + collect 流式,避免一次性载入
类型推断用 schema 参数显式声明 dtypes,防止金额/时间被误判
内存控制大分组用分批 group_by,结果 sink_csv 落盘而非常驻内存
可复现锁依赖版本 polars==x.y.z,避免内核行为漂移
性能排查用 .explain() 打印优化后查询计划,定位慢算子

补充一点排错习惯:当某步出奇地慢,先调用 .explain() 看优化器最终生成的计划,确认谓词下推和列裁剪是否生效;若发现整列被读入却只用了一两列,多半是惰性 API 没用对。把“先声明、后 collect、常看计划”三点养成肌肉记忆,Polars 的性能红利基本就能稳定拿到。

总结:Polars 用“列式内存 + 惰性优化 + Rust 并发”三板斧,把单机 DataFrame 处理推到了新量级。新项目建议直接上手 Lazy API,养成“先声明、后 collect”的习惯,性能与可读性都会更好。

上一篇 JMH 实战:Java 微基准测试精确测量性能
下一篇 eBPF 实战:用 eBPF 透视 Linux 内核与性能观测
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]