首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Polars vs Pandas:我用真实项目数据跑了个对比测试

Polars vs Pandas:我用真实项目数据跑了个对比测试

作者头像
用户11081884
发布2026-07-20 20:33:49
发布2026-07-20 20:33:49
170
举报

网上说Polars比Pandas快10倍、50倍、甚至100倍的文章满天飞,看得人一愣一愣的。

但我这个人有个毛病——别人说的数字我从来不信,非得自己跑一遍才放心。所以上周末我花了两天时间,用我们项目里的真实数据做了一轮对比测试。

跑完的结果嘛——Polars确实快,这个没什么悬念。但快多少?哪些操作快?小数据量也快吗?Pandas就真的一无是处了?这些问题,网上那些标题党文章可不会告诉你。

具体的数字都在下面,自己判断。


测试环境

先交代下硬件和数据。

机器配置:

  • CPU:Intel i7-13700H(6大核+8小核)
  • 内存:32GB DDR5
  • 硬盘:NVMe SSD
  • Python:3.11
  • Pandas:2.2.3
  • Polars:1.18.0

测试数据:

我用了三组不同规模的数据,都是从我们线上用户行为日志里脱敏出来的:

数据集

行数

列数

文件大小

50万

18

~120MB

500万

18

~1.2GB

5000万

18

~12GB

每行数据包含用户ID、事件类型、时间戳、页面URL、停留时长、设备信息、地理位置等18个字段,有数值、字符串、时间戳三种类型混合。这比很多benchmark用的纯随机数要更贴近真实场景。

计时方式: 每个操作跑5次取中位数。Pandas用 time.perf_counter(),Polars的Eager模式同样处理,Lazy模式用 .collect() 的返回时间。


测试一:读CSV

这是最基础的操作,也是大家最常比较的。

代码语言:javascript
复制
# 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_csvcollect 引入了额外的查询规划开销,对于纯读取来说这个开销没有收益。Lazy模式的优势在后面。


测试二:筛选+分组+聚合

这是日常数据处理中最常见的组合操作。我模拟的场景是:筛选出停留时长大于10秒的事件,按设备类型分组,计算每组的事件数和平均停留时长。

代码语言:javascript
复制
# 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倍。


测试三:Join操作

这个是Pandas的老大难。我造了两个表:一个是5000万行的事件表,一个是200万行的用户画像表,按 user_id 做左连接。

代码语言:javascript
复制
# 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 这一列,需要做域名提取和路径解析,都是字符串操作。

代码语言:javascript
复制
# 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也有慢的时候

公平起见,我也得说说Polars不如Pandas的地方。

小数据量的纯数值计算,Polars优势不大。

我做了一个简单的测试:对一列数值做标准化(减均值除标准差)。50万行的时候,Pandas花了0.02秒,Polars花了0.015秒。差距很小,因为这种纯数值计算NumPy本身就是高度优化的C代码,Polars的Rust后端并没有太多发挥空间。

数据量到500万行以上,Polars的多线程优势才开始明显——大概3-4倍的差距。

逐行apply操作,Polars反而更慢。

代码语言:javascript
复制
# 一个复杂的逐行判断
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:

代码语言:javascript
复制
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之类的方案?评论区聊聊。

“无他,惟手熟尔”!有需要的用起来!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nicholas与Pypi 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试环境
  • 测试一:读CSV
  • 测试二:筛选+分组+聚合
  • 测试三:Join操作
  • 测试四:字符串操作
  • 测试五:内存占用
  • 汇总:所有测试的倍数差距
  • 但是,Polars也有慢的时候
  • 一个容易被忽略的问题:写法变化
  • 我的实际选择
  • 测试代码
  • 说两句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档