Polars vs Pandas:千万级数据实测,迁移到底值不值?
同一份 1000 万行 CSV,Pandas 跑到一半内存开始顶,Polars 还在往下走。
这时候再讨论“语法优雅不优雅”,意义不大。我一般先看三个东西:读文件耗时、groupby 耗时、内存峰值。别上来就说迁移,迁移不是换个 import 那么简单。
测试脚本我一般会写得很土,但够用。
import os
import time
import psutil
import pandas as pd
import polars as plDATA_FILE = "order_1000w.csv"
N = 10_000_000
defrss_mb():
return psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024
defwatch(name, fn):
start_mem = rss_mb()
start = time.perf_counter()
result = fn()
cost = time.perf_counter() - start
end_mem = rss_mb()
print(f"{name:<22} cost={cost:.2f}s mem+={end_mem - start_mem:.1f}MB")
return result
defbuild_csv():
if os.path.exists(DATA_FILE):
return
batch = 500_000
first = True
for base in range(0, N, batch):
size = min(batch, N - base)
df = pd.DataFrame({
"order_id": range(base, base + size),
"user_id": [(base + i) % 800_000 for i in range(size)],
"shop_id": [(base + i) % 5000for i in range(size)],
"amount": [((base + i) % 3000) / 10for i in range(size)],
"status": ["paid"if (base + i) % 7else"refund"for i in range(size)]
})
df.to_csv(DATA_FILE, index=False, mode="a", header=first)
first = False
build_csv()
这里我没造复杂业务,就五个字段。订单 ID、用户 ID、店铺 ID、金额、状态。线上表当然比这个脏多了,但压读取、过滤、聚合,够看出味道。
Pandas 这边:
defpandas_job():
df = pd.read_csv(DATA_FILE) paid = df[df["status"] == "paid"]
report = (
paid.groupby("shop_id", as_index=False)
.agg(
order_cnt=("order_id", "count"),
user_cnt=("user_id", "nunique"),
amount_sum=("amount", "sum")
)
)
return report.sort_values("amount_sum", ascending=False).head(20)
watch("pandas csv groupby", pandas_job)
Polars 这边我不会一上来就 read_csv,因为那样只用了它一半能力。大文件我更愿意先试 lazy。
defpolars_job():
report = (
pl.scan_csv(DATA_FILE)
.filter(pl.col("status") == "paid")
.group_by("shop_id")
.agg([
pl.len().alias("order_cnt"),
pl.col("user_id").n_unique().alias("user_cnt"),
pl.col("amount").sum().alias("amount_sum")
])
.sort("amount_sum", descending=True)
.limit(20)
.collect()
)return report
watch("polars lazy groupby", polars_job)
跑完以后,差距通常不会太含蓄。
Pandas 的问题不是它慢一点,而是它很容易把整份数据先吞进内存。你后面哪怕只要 status = paid 的数据,它也先把 CSV 读成一个完整 DataFrame。这个动作在百万级没什么,到了千万级就开始有感觉。
Polars 的 lazy 这块比较狠,它会把过滤、聚合、投影往前推。你只要三个字段,它就不会傻乎乎把所有列都完整拉进来再说。这种差别,不是写法差别,是执行计划差别。
我当时第一眼不信的是 join。
因为很多文章只测 groupby,groupby 确实容易让 Polars 好看。但业务里更烦的是宽表拼接,比如订单表拼用户画像、拼店铺维表、拼活动表。这个地方才容易把内存打穿。
defpolars_join_job():
orders = pl.scan_csv(DATA_FILE).filter(pl.col("status") == "paid") shops = pl.DataFrame({
"shop_id": range(5000),
"shop_level": [i % 5for i in range(5000)]
}).lazy()
return (
orders.join(shops, on="shop_id", how="left")
.group_by("shop_level")
.agg([
pl.len().alias("cnt"),
pl.col("amount").sum().alias("gmv")
])
.collect()
)
watch("polars lazy join", polars_join_job)
Pandas 也能写,代码甚至更顺手。
defpandas_join_job():
orders = pd.read_csv(DATA_FILE)
orders = orders[orders["status"] == "paid"] shops = pd.DataFrame({
"shop_id": range(5000),
"shop_level": [i % 5for i in range(5000)]
})
merged = orders.merge(shops, on="shop_id", how="left")
return (
merged.groupby("shop_level", as_index=False)
.agg(cnt=("order_id", "count"), gmv=("amount", "sum"))
)
watch("pandas join", pandas_join_job)
这段如果放在线上离线任务里,我会更偏向 Polars。不是因为 Pandas 不行,而是 Pandas 太容易在中间态把内存吃胖。一个 merge 后面接 groupby,中间 DataFrame 会实打实存在。数据量再翻一倍,机器就开始骂人。
但迁移也不是无脑值。
Pandas 有几个地方还是稳。第一,生态老,异常好搜。第二,很多同事都会,排查成本低。第三,你要是做小数据探索、画图、临时分析,Pandas 仍然舒服。几万行、几十万行的数据,换 Polars 不一定能省多少时间,反而多出一堆语法适配。
我更建议按任务迁,不要按项目迁。
比如这些任务,值得动:
1. CSV / Parquet 超过几 GB
2. groupby、join、窗口计算很多
3. 任务经常因为内存被 kill
4. 只需要读部分列,但 Pandas 每次全量读
5. 数据清洗链路长,中间 DataFrame 一层套一层
这些就先别急:
1. 数据量不到百万
2. 主要靠 Pandas 生态里的老库
3. 团队里没人熟 Polars
4. 代码里大量 apply,逻辑又很碎
尤其是 apply。Pandas 里很多人喜欢一把梭,什么都塞进 apply。迁到 Polars 后如果还这么写,性能不一定好看。Polars 更吃表达式写法。
比如这种 Pandas 代码:
df["risk_tag"] = df.apply(
lambda r: "high"if r["amount"] > 500and r["status"] == "refund"else"normal",
axis=1
)
迁 Polars 我会改成这样:
df = df.with_columns(
pl.when((pl.col("amount") > 500) & (pl.col("status") == "refund"))
.then(pl.lit("high"))
.otherwise(pl.lit("normal"))
.alias("risk_tag")
)
这不是语法洁癖。表达式能被优化,逐行 Python 函数不行。这个坑不提前说,迁完以后很容易得出一个奇怪结论:Polars 也没多快。
我的判断比较简单:千万级数据,Polars 值得试,而且最好先试在离线清洗、报表聚合、数据导入导出这些地方。线上核心链路别一下子全换,先拿一条慢任务开刀,保留输入输出一致,再对账。
迁移最终看两张表:耗时表,内存表。
只要这两张表能把机器省下来,把任务稳定性提上去,迁就值。要是只是为了写法新鲜,把 Pandas 全部推倒重写,那八成是在给自己找活。