
网上说Polars比Pandas快10倍、50倍、甚至100倍的文章满天飞,看得人一愣一愣的。
但我这个人有个毛病——别人说的数字我从来不信,非得自己跑一遍才放心。所以上周末我花了两天时间,用我们项目里的真实数据做了一轮对比测试。
跑完的结果嘛——Polars确实快,这个没什么悬念。但快多少?哪些操作快?小数据量也快吗?Pandas就真的一无是处了?这些问题,网上那些标题党文章可不会告诉你。
具体的数字都在下面,自己判断。
先交代下硬件和数据。
机器配置:
测试数据:
我用了三组不同规模的数据,都是从我们线上用户行为日志里脱敏出来的:
数据集 | 行数 | 列数 | 文件大小 |
|---|---|---|---|
小 | 50万 | 18 | ~120MB |
中 | 500万 | 18 | ~1.2GB |
大 | 5000万 | 18 | ~12GB |
每行数据包含用户ID、事件类型、时间戳、页面URL、停留时长、设备信息、地理位置等18个字段,有数值、字符串、时间戳三种类型混合。这比很多benchmark用的纯随机数要更贴近真实场景。
计时方式: 每个操作跑5次取中位数。Pandas用 time.perf_counter(),Polars的Eager模式同样处理,Lazy模式用 .collect() 的返回时间。
这是最基础的操作,也是大家最常比较的。
# Pandas
df = pd.read_csv("events.csv")
# Polars Eager
df = pl.read_csv("events.csv")
# Polars Lazy
df = pl.scan_csv("events.csv").collect()结果:
数据集 | Pandas | Polars Eager | Polars Lazy |
|---|---|---|---|
小(50万行) | 3.2s | 0.8s | 0.9s |
中(500万行) | 38s | 6.1s | 6.4s |
大(5000万行) | 6分12s | 52s | 48s |
读CSV这个环节,Polars的优势很明显——基本是4-8倍的加速。原因没什么悬念:Polars底层是多线程解析,Pandas是单线程。CSV解析这种IO密集+CPU密集的操作,多核直接拉满。
有个小意外:Polars Lazy模式在读CSV上并没有比Eager快,反而有时候慢一点点。这是因为 scan_csv 加 collect 引入了额外的查询规划开销,对于纯读取来说这个开销没有收益。Lazy模式的优势在后面。
这是日常数据处理中最常见的组合操作。我模拟的场景是:筛选出停留时长大于10秒的事件,按设备类型分组,计算每组的事件数和平均停留时长。
# Pandas
result = (
df[df['duration'] >10]
.groupby('device_type')
.agg(
count=('duration', 'count'),
avg_duration=('duration', 'mean')
)
.reset_index()
)
# Polars Eager
result = (
df.filter(pl.col('duration') >10)
.group_by('device_type')
.agg([
pl.col('duration').count().alias('count'),
pl.col('duration').mean().alias('avg_duration'),
])
)
# Polars Lazy
result = (
pl.scan_csv("events.csv")
.filter(pl.col('duration') >10)
.group_by('device_type')
.agg([
pl.col('duration').count().alias('count'),
pl.col('duration').mean().alias('avg_duration'),
])
.collect()
)结果:
数据集 | Pandas | Polars Eager | Polars Lazy |
|---|---|---|---|
小(50万行) | 0.45s | 0.08s | 0.06s |
中(500万行) | 5.8s | 0.7s | 0.5s |
大(5000万行) | 82s | 8.2s | 5.1s |
这个测试里Polars Lazy模式的优势开始显现了——因为 scan_csv 会把过滤条件下推到读取阶段,只读满足 duration > 10 的行。5000万行数据里大概只有60%满足条件,相当于直接省掉了40%的IO和内存。
Pandas在这个操作上的劣势是双重叠加的:筛选是单线程的,分组聚合也是单线程的。
倍数差距:小数据集5-7倍,大数据集10-16倍。
这个是Pandas的老大难。我造了两个表:一个是5000万行的事件表,一个是200万行的用户画像表,按 user_id 做左连接。
# Pandas
result = events.merge(users, on='user_id', how='left')
# Polars
result = events.join(users, on='user_id', how='left')结果:
数据集 | Pandas | Polars Eager | Polars Lazy |
|---|---|---|---|
小(50万行) | 1.8s | 0.3s | 0.2s |
中(500万行) | 28s | 2.8s | 1.9s |
大(5000万行) | 内存爆了 | 31s | 22s |
对,你没看错——5000万行的Join,Pandas直接OOM了。
32GB内存不够用。Pandas做merge的时候需要创建好几份中间数据的拷贝(索引、排序、结果集),内存占用大约是原始数据的4-6倍。5000万行×18列的事件表在内存里大概占12GB,乘以5倍就是60GB,直接爆了。
Polars跑完了,而且只用了不到14GB内存。列式存储+零拷贝的设计在这种大表join场景下优势巨大。
这也是我测试中唯一一个Pandas“做不到”而Polars“能做到”的场景。
这个测试可能很多人不会想到去比。我们的数据里有 page_url 这一列,需要做域名提取和路径解析,都是字符串操作。
# Pandas
df['domain'] = df['page_url'].str.split('/').str[2]
df['is_mobile'] = df['user_agent'].str.contains('Mobile|Android|iPhone')
# Polars
df = df.with_columns([
pl.col('page_url').str.split('/').list.get(2).alias('domain'),
pl.col('user_agent').str.contains('Mobile|Android|iPhone').alias('is_mobile'),
])结果(只跑了中等数据集,500万行):
操作 | Pandas | Polars |
|---|---|---|
split+取值 | 12.3s | 0.9s |
contains正则匹配 | 8.7s | 0.4s |
字符串操作是Polars碾压最狠的地方。13-22倍的差距。
Pandas的字符串操作一直是最被人吐槽的弱点——底层是用Python的 str 方法逐元素调用的,完全没有走NumPy的向量化。Polars的字符串操作走的是Rust的并行字符串处理,差距自然就出来了。
如果你的管道里有大量字符串处理,光换Polars就能把这一块的性能从分钟级拉到秒级。
这个我专门量了一下,因为很多人只关注速度,忽略了内存。
操作:读取500万行数据,然后做筛选+聚合。记录峰值内存占用。
环节 | Pandas | Polars Eager | Polars Lazy |
|---|---|---|---|
读取CSV后 | 2.1GB | 1.4GB | 1.4GB |
筛选后 | 2.1GB | 1.4GB | 0.8GB |
聚合后 | 2.1GB | 1.4GB | 0.8GB |
几个发现:
Pandas的内存管理比较“粗放”,筛选操作会创建一份新的DataFrame拷贝,但原始的那份还在。所以峰值内存基本不会下降。
Polars Eager模式用的是Arrow列式存储,同样的数据本身就更紧凑(字符串存储效率尤其高),所以基础内存占用就少30%左右。
Polars Lazy模式的优势在这里最明显——因为谓词下推和投影裁剪,它实际读入内存的数据量远小于整个CSV。500万行只读了满足条件的约300万行,而且只读了用到的列。
操作 | 小数据(50万行) | 中数据(500万行) | 大数据(5000万行) |
|---|---|---|---|
读CSV | 4x | 6x | 7x |
筛选+聚合 | 5x | 8x | 16x |
Join | 6x | 10x | Pandas OOM |
字符串操作 | — | 13x | — |
内存占用 | 少30% | 少30-60% | 少60%+ |
结果一目了然——Polars在几乎所有场景下都更快,数据量越大差距越明显。不过小数据量的时候,优势确实没那么夸张。
公平起见,我也得说说Polars不如Pandas的地方。
小数据量的纯数值计算,Polars优势不大。
我做了一个简单的测试:对一列数值做标准化(减均值除标准差)。50万行的时候,Pandas花了0.02秒,Polars花了0.015秒。差距很小,因为这种纯数值计算NumPy本身就是高度优化的C代码,Polars的Rust后端并没有太多发挥空间。
数据量到500万行以上,Polars的多线程优势才开始明显——大概3-4倍的差距。
逐行apply操作,Polars反而更慢。
# 一个复杂的逐行判断
def classify_row(row):
if row['amount'] >1000 and row['days'] >30:
return'VIP'
elif row['amount'] >500:
return '活跃'
return '普通'
# Pandas
df['label'] = df.apply(classify_row, axis=1)
# Polars
df = df.with_columns(
pl.struct(['amount', 'days'])
.map_elements(lambda s: classify_row(s), return_dtype=pl.Utf8)
.alias('label')
)500万行数据,Pandas的 apply 跑了18秒,Polars的 map_elements 跑了22秒。
Polars自己说的,map_elements 会“break out of the query engine”——也就是说它会放弃Rust的并行优化,回退到Python逐行调用。这种情况下它甚至比Pandas还慢一点,因为多了一层Polars的封装开销。
当然,如果你的逻辑能翻译成Polars表达式(when/then/otherwise),那就是另一个故事了——0.1秒就跑完了。
性能只是一方面。迁移到Polars之后,我发现代码风格也在潜移默化地变化。
Pandas鼓励的是一种“探索式”的写法——你在Jupyter里一行一行地试,每行都立即看到结果,改一下再跑。这在小数据量下很舒服,但养成了一些坏习惯:频繁地创建中间变量、大量的 inplace 操作、到处写 df = df[...]。
Polars的链式表达式写法天然鼓励你把整个数据处理流程写成一个完整的pipeline:
result = (
pl.scan_csv("events.csv")
.filter(pl.col('status') == 'active')
.with_columns(
pl.col('created_at').str.to_datetime().alias('created_dt'),
)
.group_by('device_type')
.agg([
pl.col('amount').sum().alias('total'),
pl.col('user_id').n_unique().alias('unique_users'),
])
.sort('total', descending=True)
.collect()
)整段代码读下来,从头到尾就是一个完整的数据流——读数据、过滤、衍生字段、聚合、排序。没有中间变量,没有 inplace,没有 reset_index()。
这种写法在代码review和debug的时候特别友好——你一眼就能看出数据经过了哪些变换,而不需要在十几个 df_xxx 变量之间跳来跳去。
跑完这些测试之后,我做了一个不算意外但很务实的决定:
核心ETL管道全面切换到Polars。 每天处理的千万级用户行为日志,从读取到聚合到输出,全部用Polars Lazy模式。管道整体耗时从14分钟压到了2分钟出头。
分析师的交互探索继续使用Pandas。 Jupyter Notebook里的数据探查、画图、临时分析,Pandas的生态和直觉性还是最好的。分析师不需要关心性能,她们要的是灵活和顺手。
机器学习建模前的特征工程用Polars。 特征计算涉及大量的分组聚合和字符串处理,正好是Polars最强的领域。算完之后 .to_pandas() 转成DataFrame直接喂给sklearn。
说白了就是——谁擅长什么就让谁上。两个库在我的项目里不是替代关系,是互补关系。
完整的测试代码和数据生成脚本我整理好了,感兴趣的话可以照着我的思路自己搭一套。建议用跟我类似规模的数据,小数据量的差异不明显,大数据量才能看出真正的差距。
跑之前注意两件事:一是确保Pandas和Polars都是最新版本,二是大数据集测试至少需要16GB内存,否则Pandas那边会OOM(别问我怎么知道的)。
跑完这些测试我最大的感受是——光看网上的文章没用,benchmark的数字永远只是参考,真正决定你该不该换的是你自己的数据、自己的管道、自己的团队习惯。
我现在最痛的ETL环节已经全换Polars了,效果立竿见影。但分析师那边的Jupyter Notebook我一行没动,人家Pandas用得好好的,我折腾什么。
如果你正在纠结要不要试Polars,我的建议是:先找一个你最慢的管道环节,用Polars重写一下对比看。别一上来就搞全面迁移,那个工程量会劝退你的。
你们项目里是怎么处理Pandas性能瓶颈的?有上Polars的吗?还是用的Dask之类的方案?评论区聊聊。
“无他,惟手熟尔”!有需要的用起来!
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!