Polars 是一个用 Rust 实现的 DataFrame 库,主打性能。和 Pandas 做了一组详细的对比测试,结果值得参考。
Polars 简介
Polars 由 Ritchie Vink(原 Pandas 贡献者)开发,底层用 Rust 实现,通过 PyO3 提供 Python 接口。核心卖点:
- Rust 实现:充分利用 SIMD 指令和多核并行
- Lazy Evaluation:先构建查询计划,再优化执行
- Apache Arrow 内存格式:列式存储,对分析查询友好
- 不依赖 NumPy:整个技术栈独立于 Python C 扩展
安装很简单:
pip install polars
基本语法对比
先看读取 CSV:
import pandas as pd
import polars as pl
# Pandas
df_pd = pd.read_csv("sales.csv")
# Polars
df_pl = pl.read_csv("sales.csv")
看起来差不多。但 Polars 在读取时就利用多线程并行解析,大文件差异明显。
过滤
# Pandas
result_pd = df_pd[df_pd["amount"] > 1000]
# 或者
result_pd = df_pd.query("amount > 1000")
# Polars
result_pl = df_pl.filter(pl.col("amount") > 1000)
Polars 用 pl.col() 表达式来引用列,语法上比 Pandas 更统一。Pandas 有多种方式做同一件事(布尔索引、.query()、.loc[]),Polars 统一用表达式 API。
分组聚合
# Pandas
result_pd = (
df_pd.groupby("category")
.agg(
total=("amount", "sum"),
count=("amount", "count"),
avg_amount=("amount", "mean"),
)
.sort_values("total", ascending=False)
)
# Polars
result_pl = (
df_pl.group_by("category")
.agg(
total=pl.col("amount").sum(),
count=pl.col("amount").count(),
avg_amount=pl.col("amount").mean(),
)
.sort("total", descending=True)
)
Polars 的表达式更灵活——可以在 agg() 中写任意复杂的表达式,而 Pandas 的 agg() 对自定义聚合的支持较弱。
连接
# Pandas
result_pd = pd.merge(orders_pd, customers_pd, on="customer_id", how="left")
# Polars
result_pl = orders_pl.join(customers_pl, on="customer_id", how="left")
新增列
# Pandas
df_pd["profit"] = df_pd["revenue"] - df_pd["cost"]
df_pd["margin"] = df_pd["profit"] / df_pd["revenue"]
# Polars
df_pl = df_pl.with_columns(
profit=pl.col("revenue") - pl.col("cost"),
margin=(pl.col("revenue") - pl.col("cost")) / pl.col("revenue"),
)
Polars 的 with_columns 不会原地修改 DataFrame,返回新对象。这符合函数式编程的不可变理念。
Lazy Evaluation
Polars 最强大的特性是 lazy evaluation。在 eager 模式下,每一步操作立即执行;在 lazy 模式下,操作被记录为查询计划,最后一次性优化和执行。
# Lazy 模式
result = (
pl.scan_csv("large_file.csv") # 返回 LazyFrame,不读取数据
.filter(pl.col("year") >= 2024) # 记录过滤条件
.group_by("region") # 记录分组
.agg(pl.col("sales").sum()) # 记录聚合
.sort("sales", descending=True) # 记录排序
.collect() # 触发执行
)
Polars 的查询优化器会做几件事:
- 谓词下推(Predicate Pushdown):将过滤条件推到数据源层面,只读取满足条件的行
- 投影下推(Projection Pushdown):只读取需要的列,跳过无关列
- 公共子表达式消除:避免重复计算
- 并行执行:自动拆分数据,多线程处理
对于大文件,lazy 模式可以避免全量加载到内存,这是和 Pandas 最大的区别之一。
性能基准测试
用一个 500 万行、10 列的合成数据集做测试,机器是 8 核 16G 内存:
import time
import numpy as np
# 生成测试数据
n_rows = 5_000_000
data = {
"id": range(n_rows),
"category": np.random.choice(["A", "B", "C", "D", "E"], n_rows),
"region": np.random.choice(["East", "West", "North", "South"], n_rows),
"amount": np.random.uniform(10, 10000, n_rows),
"quantity": np.random.randint(1, 100, n_rows),
"date": np.random.choice(pd.date_range("2020-01-01", "2024-12-31"), n_rows),
}
df_pd = pd.DataFrame(data)
df_pl = pl.DataFrame(data)
测试 1:过滤
# Pandas: 42ms
result = df_pd[df_pd["amount"] > 5000]
# Polars: 11ms (3.8x faster)
result = df_pl.filter(pl.col("amount") > 5000)
测试 2:分组聚合
# Pandas: 380ms
result = df_pd.groupby(["category", "region"]).agg({"amount": ["sum", "mean"], "quantity": "sum"})
# Polars: 65ms (5.8x faster)
result = df_pl.group_by(["category", "region"]).agg(
amount_sum=pl.col("amount").sum(),
amount_mean=pl.col("amount").mean(),
quantity_sum=pl.col("quantity").sum(),
)
测试 3:排序
# Pandas: 850ms
result = df_pd.sort_values(["category", "amount"], ascending=[True, False])
# Polars: 195ms (4.4x faster)
result = df_pl.sort(["category", "amount"], descending=[False, True])
测试 4:Join
# 两个 DataFrame join
# Pandas: 1200ms
result = pd.merge(df_pd, lookup_pd, on="category", how="left")
# Polars: 180ms (6.7x faster)
result = df_pl.join(lookup_pl, on="category", how="left")
测试 5:CSV 读取(1GB 文件)
# Pandas: 8.2s
df = pd.read_csv("large.csv")
# Polars: 1.8s (4.6x faster)
df = pl.read_csv("large.csv")
总体来看,Polars 在大多数操作上快 3-7 倍。数据量越大,差距越明显。
内存使用对比
Polars 使用 Arrow 列式格式,同样的数据通常占用更少的内存:
import sys
# 同一份 500 万行数据
# Pandas: ~420 MB
print(df_pd.memory_usage(deep=True).sum() / 1024**2)
# Polars: ~280 MB
print(df_pl.estimated_size("mb"))
差距主要来自字符串列:Pandas 用 Python 对象数组存储字符串(每个字符串是一个 Python 对象),Polars 用 Arrow 的二进制格式(连续内存 + 偏移量数组)。
什么时候用 Polars,什么时候用 Pandas
选 Polars 的场景:
- 数据量大(百万行以上)
- 需要高性能的 ETL 管道
- 计算密集的分组聚合
- 内存受限环境
- 新项目,没有 Pandas 历史代码
继续用 Pandas 的场景:
- 已有大量 Pandas 代码
- 需要和 scikit-learn、statsmodels 等库深度集成(这些库的输入输出是 Pandas DataFrame)
- 数据量小,性能不是瓶颈
- 需要 Pandas 独有的功能(比如 MultiIndex、某些时间序列操作)
两者不是非此即彼。Polars 可以和 Pandas 互转:
# Polars -> Pandas
df_pd = df_pl.to_pandas()
# Pandas -> Polars
df_pl = pl.from_pandas(df_pd)
在实际项目中,可以在数据处理阶段用 Polars 提速,在对接机器学习库时转回 Pandas。