polars 替代 pandas 处理大型 CSV / Parquet(性能 + API 都更好)

pandas 是 2010 年代标准工具但有几个本质局限:

  • 单线程(GIL)
  • eager evaluation,复杂 pipeline 中间结果都实例化
  • API 历史包袱(10 种 index、SettingWithCopyWarning、inplace=True

polars 是 Rust 写的列存数据框,多线程 + lazy 模式 + 干净的链式 API。
1GB+ 的 CSV 处理快 5-30 倍。

安装

uv add polars pyarrow

pyarrow 不是必需但读 Parquet / Feather 时性能好。

30 秒上手

import polars as pl

df = pl.read_csv('sales.csv')
print(df.head())
print(df.schema)

# 过滤 + 聚合 + 排序
result = (
    df.filter(pl.col('country') == 'CN')
      .group_by('product')
      .agg(
          pl.col('amount').sum().alias('total'),
          pl.col('amount').count().alias('orders'),
      )
      .sort('total', descending=True)
      .head(10)
)
print(result)

注意:

  • pl.col('x') 是表达式,可以组合(pl.col('x') * 2
  • group_by().agg() 链式
  • .alias() 重命名
  • 不像 pandas 那样有索引;纯列数据

lazy 模式

# 用 scan_csv 而不是 read_csv 进 lazy 模式
q = (
    pl.scan_csv('sales.csv')
      .filter(pl.col('country') == 'CN')
      .group_by('product')
      .agg(pl.col('amount').sum())
      .sort('amount', descending=True)
)

# 这一步还没读文件!只构建了 query plan
print(q.explain())
# 看到优化后的 plan(如 filter 下推)

# 真正执行 + 取结果
result = q.collect()

lazy 让 polars 做查询优化:predicate pushdown、projection pushdown、
predicate fusion 等。处理 10GB CSV 时 collect 之前都不占内存。

Parquet:列存的好处

# 写 Parquet
df.write_parquet('sales.parquet', compression='zstd')

# 读
df = pl.read_parquet('sales.parquet')

# lazy + projection
q = (pl.scan_parquet('sales.parquet')
       .select(['country', 'amount'])    # 只读这两列
       .filter(pl.col('country') == 'US')
       .group_by('country').agg(pl.col('amount').sum())
       .collect())

Parquet 列存意味着 .select(['country', 'amount']) 完全不读其它列。
10GB 表只读 2 列可能只 IO 1GB。

性能对比

任务(1GB CSV,5000 万行) pandas polars eager polars lazy
读文件 18s 6s 0.5s (scan)
filter + groupby + agg 25s 4s 3s
内存峰值 8GB 3GB 1.5GB

数字会因数据 / 机器而异,但量级对。

常用对照表

pandas polars
df['col'] df['col']df.get_column('col')
df[df['x'] > 0] df.filter(pl.col('x') > 0)
df.groupby('a')['b'].sum() df.group_by('a').agg(pl.col('b').sum())
df.merge(other, on='id') df.join(other, on='id')
df['col'].str.lower() df.with_columns(pl.col('col').str.to_lowercase())
df.dropna() df.drop_nulls()
df.fillna(0) df.fill_null(0)
pd.concat([df1, df2]) pl.concat([df1, df2])
df.pivot_table df.pivot()

窗口函数

# 按 user 排序后的 cumsum
df = df.with_columns(
    pl.col('amount').cum_sum().over('user').alias('cum_amount')
)

# 计算每行相对所在组的平均
df = df.with_columns(
    (pl.col('amount') / pl.col('amount').mean().over('country')).alias('rel')
)

.over(...) 是 partition by 的简洁写法。

与 pandas 互转

# polars → pandas
pdf = df.to_pandas()

# pandas → polars
df2 = pl.from_pandas(pdf)

适合渐进迁移:现有 pandas 流程改一段为 polars 跑性能瓶颈。

与 Arrow / DuckDB 联用

polars 内部就是 Arrow 格式,零拷贝传给 DuckDB / Arrow Compute:

import duckdb
df = pl.read_parquet('big.parquet')
# 直接把 polars df 当 DuckDB 视图
result = duckdb.sql("SELECT country, SUM(amount) FROM df GROUP BY 1").pl()

DuckDB 跑 SQL,polars 拿结果,全程 Arrow buffer。

写入数据库

import polars as pl

df = pl.read_csv('users.csv')
df.write_database(
    table_name='users',
    connection='postgresql://localhost/mydb',
    if_table_exists='replace',
)

底层用 ConnectorX / SQLAlchemy 写。

何时仍用 pandas

  • ML 库强制:scikit-learn 接受 numpy / pandas,polars 要 .to_numpy()
  • 小数据 + 现有代码:pandas 足够时迁移没收益
  • 复杂的 multi-level index 需求

不过 polars 团队明确目标是覆盖 pandas 90% 的用法,差距越来越小。

踩过的坑

  • 没有 index 的概念:以前 pandas df.loc['2024-01-01'] 这种行为不存在。
    polars 都用 column filter。
  • with_columns 返回新 DataFrame(immutable),忘了赋值回去 df = ...
    python df.with_columns(pl.col('x') * 2) # 没改 df! df = df.with_columns(pl.col('x') * 2) # 对
  • group_by 的 Python 列表语法 → 改成表达式:
    python # pandas df.groupby(['a', 'b']).agg({'c': 'sum'}) # polars df.group_by(['a', 'b']).agg(pl.col('c').sum())
  • 日期解析自动推断不总是对:pl.read_csv(..., try_parse_dates=True)
    或者显式 pl.col('date').str.to_datetime('%Y-%m-%d')
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。