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')。
登录后参与评论。