Python Polars vs Pandas性能对比

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。