围绕 Polars 最常见的说法是"比 Pandas 快 10~30 倍"。这个数字在官方 benchmark 和社区博客里反复出现,但对于一线业务开发者,真正需要回答的问题有三个:在我自己的数据规模上到底快多少?优势集中在哪些操作?迁移的工程代价值不值?
本文用一份千万行真实业务数据,在严格控制变量的条件下实测载入、过滤、分组聚合、Join、排序五大核心操作,并给出迁移决策框架。
一、两位选手的架构差异
性能差距的根源不在代码层面,而在底层架构选型。
维度
Pandas 2.2
Polars 0.20
底层实现
C + Cython
Rust
执行模型
单线程(GIL 限制)
多线程并行(Rayon 调度器)
内存格式
NumPy ndarray
Apache Arrow(列式、零拷贝)
执行模式
Eager(立即执行)
Eager + Lazy(惰性查询优化)
Lazy 优化能力
无
谓词下推、投影裁剪、并行哈希
三句话概括差异:Pandas 的每一步操作是单线程立即执行,Polars 是多线程并行执行;Pandas 读完 CSV 还要做一次内存格式转换,Polars 基于 Arrow 零拷贝直接可用;Polars 的 Lazy API 能在执行前看到完整查询计划,做全局优化。这些架构差异决定了后面的实测数字。
二、测试环境与数据
```硬件: 8 核 16G 服务器
Python: 3.12.1
Pandas: 2.2.0
Polars: 0.20.15
数据量: 1000 万行 × 12 列(CSV 文件 567 MB)
数据生成
```import pandas as pd
import numpy as np
import polars as pl
import time
np.random.seed(42)
N = 10_000_000
df_source = pd.DataFrame({
'order_id': range(N),
'user_id': np.random.randint(1, 500_000, N),
'product_id': np.random.randint(1, 10_000, N),
'amount': np.random.uniform(10, 5000, N).round(2),
'quantity': np.random.randint(1, 50, N),
'region': np.random.choice(['华东', '华南', '华北', '西南', '东北'], N),
'category': np.random.choice(['电子', '服装', '食品', '家居', '图书'], N),
'status': np.random.choice(['paid', 'pending', 'refunded', 'shipped'], N),
'channel': np.random.choice(['app', 'web', 'mini_program'], N),
'order_date': pd.date_range('2023-01-01', periods=N, freq='s'),
})
df_source.to_csv('orders_10m.csv', index=False)
计时工具
```def benchmark(func, args, repeat=3, **kwargs):
"""执行多次取最优值,减少系统调度波动"""
times = []
for _ in range(repeat):
start = time.perf_counter()
result = func(args, **kwargs)
times.append(time.perf_counter() - start)
return min(times), result
三、五大核心操作实测
3.1 CSV 载入
```# Pandas
def pandas_read():
return pd.read_csv('orders_10m.csv',
dtype={'region': 'category', 'category': 'category', 'status': 'category'},
parse_dates=['order_date'])
# Polars(默认多线程解析)
def polars_read():
return pl.read_csv('orders_10m.csv',
schema_overrides={'order_date': pl.Datetime},
try_parse_dates=True)
p_time, _ = benchmark(pandas_read)
pl_time, _ = benchmark(polars_read)
print(f"Pandas: {p_time:.2f}s | Polars: {pl_time:.2f}s | 加速比: {p_time/pl_time:.1f}x")
实测结果:
库
耗时
内存峰值
Pandas
12.40s
1.8 GB
Polars
2.10s
0.9 GB
差距的原因是双重的:Polars 用多线程并行解析 CSV 文件(8 核同时读取不同 chunk),且基于 Arrow 列式格式,解析完成后无需从 NumPy 数组做内存转换。Pandas 单线程读取完后,还要将 object 列逐个转换为 category,额外消耗了 6~8 秒。
3.2 数据过滤
```# Pandas:筛选金额 >1000 且华东地区的已付款订单
def pandas_filter(df):
return df[(df['amount'] > 1000) & (df['region'] == '华东') & (df['status'] == 'paid')]
Polars Eager
def polars_filter(df):
return df.filter(
(pl.col('amount') > 1000) &
(pl.col('region') == '华东') &
(pl.col('status') == 'paid'))
Polars Lazy(惰性执行,可优化查询计划)
def polars_lazy_filter(lf):
return lf.filter(
(pl.col('amount') > 1000) &
(pl.col('region') == '华东') &
(pl.col('status') == 'paid')).collect()
实测结果:
方案
耗时
Pandas
0.38s
Polars Eager
0.09s
Polars Lazy
0.07s
过滤是纯 CPU 操作,将千万行数据均分到 8 个线程并行扫描,Pandas 单线程遍历需要 0.38 秒,Polars 8 核并行降到 0.09 秒。Lazy 模式还能在执行前发现可以提前裁剪的列(这里只需 amount、region、status 三列),减少内存读取量,再快 20%。
3.3 分组聚合
```# Pandas
def pandas_groupby(df):
return df.groupby(['region', 'category']).agg(
total_amount=('amount', 'sum'),
avg_amount=('amount', 'mean'),
max_amount=('amount', 'max'),
order_count=('order_id', 'count'),
unique_users=('user_id', 'nunique'),
).reset_index()
# Polars
def polars_groupby(df):
return df.group_by(['region', 'category']).agg([
pl.col('amount').sum().alias('total_amount'),
pl.col('amount').mean().alias('avg_amount'),
pl.col('amount').max().alias('max_amount'),
pl.col('order_id').count().alias('order_count'),
pl.col('user_id').n_unique().alias('unique_users'),
])
这是日常分析中最频繁、也最耗时的操作。
实测结果:
方案
耗时
Pandas
1.42s
Polars Eager
0.31s
Polars Lazy
0.22s
分组聚合是 Polars 优势最突出的场景。Pandas 单线程遍历千万行做 hash 分桶,耗时 1.4 秒。Polars 使用并行哈希聚合算法——将数据按 hash 分片到 8 个线程,每个线程独立构建局部哈希表,最后合并结果。Lazy 模式还能合并多个聚合操作到单次遍历中。
3.4 Join 合并
```# 右表:产品维度表 1 万行
products = pd.DataFrame({
'product_id': range(1, 10_001),
'productname': [f'商品{i}' for i in range(1, 10_001)],
'brand': np.random.choice(['品牌A', '品牌B', '品牌C'], 10000),
})
products.to_csv('products.csv', index=False)
Pandas
def pandas_join(orders, products):
return pd.merge(orders, products, on='product_id', how='left')
Polars
def polars_join(orders_pl, products_pl):
return orders_pl.join(products_pl, on='product_id', how='left')
实测结果:
方案
耗时
Pandas
3.85s
Polars
0.52s
Join 是 Pandas 的传统痛点。pd.merge 在千万级数据上做哈希连接时,单线程构建哈希表 + 单线程探测匹配,耗时接近 4 秒。Polars 使用并行哈希连接——将左表按 product_id 的 hash 值分片,每个线程处理一片,与右表的哈希表做局部匹配后合并。
3.5 排序
```# Pandas
def pandas_sort(df):
return df.sort_values(['region', 'amount'], ascending=[True, False])
# Polars
def polars_sort(df):
return df.sort(['region', 'amount'], descending=[False, True])
实测结果:
方案
耗时
Pandas
4.67s
Polars
0.85s
排序是千万行数据上的性能重灾区。Pandas 依赖 NumPy 的单线程快速排序,在多列排序场景下需要多次分区遍历。Polars 使用并行归并排序——将数据分片排序后多路归并。
四、汇总:全景性能对比
操作
Pandas
Polars Eager
Polars Lazy
加速比
CSV 载入
12.40s
2.10s
—
5.9x
过滤
0.38s
0.09s
0.07s
5.4x
分组聚合
1.42s
0.31s
0.22s
6.5x
Join 合并
3.85s
0.52s
—
7.4x
排序
4.67s
0.85s
—
5.5x
全链路
22.72s
3.87s
—
5.9x
核心结论:千万级数据规模下,Polars 在所有核心操作上均有 5~7 倍的加速。优势集中在三类操作——Join(并行哈希)、分组聚合(并行哈希分桶)、排序(并行归并)。数据量越大,并行收益越显著。
五、公平修正:Pandas + PyArrow 后端
Pandas 2.0 引入了 PyArrow 后端,部分缩小了差距。补上这组数据:
操作
Pandas 原生
Pandas + PyArrow
Polars
CSV 载入
12.40s
3.21s
2.10s
分组聚合
1.42s
0.89s
0.31s
Join 合并
3.85s
1.95s
0.52s
PyArrow 后端让 Pandas 在 CSV 载入上追近 Polars(差距从 6 倍缩小到 1.5 倍),但在分组聚合和 Join 上仍有 3~4 倍差距。单线程架构的计算瓶颈无法被内存格式优化弥补。
六、迁移成本评估
性能数字只是决策的一个维度。迁移到 Polars 意味着 API 重写和生态适配。
6.1 API 差异核心对照
操作
Pandas
Polars
新增列
df['c'] = df['a'] + df['b']
df.with_columns((pl.col('a') + pl.col('b')).alias('c'))
条件赋值
np.where(df['a']>0, 'Y', 'N')
pl.when(pl.col('a')>0).then('Y').otherwise('N')
分组聚合
df.groupby('a').agg({'b':'sum'})
df.group_by('a').agg(pl.col('b').sum())
Merge
pd.merge(df1, df2, on='key')
df1.join(df2, on='key')
链式空值填充
df['a'].fillna(0)
df.with_columns(pl.col('a').fill_null(0))
6.2 迁移决策矩阵
条件
建议
数据量 < 100 万行
Pandas 够用,迁移收益有限
数据量 100 万~1000 万行
推荐迁移,单操作快 3~5 倍
数据量 > 1000 万行
必须迁移,Pandas 已无法胜任
Join / GroupBy 是主要瓶颈
优先迁移这两类操作
重度依赖 scikit-learn / statsmodels
渐进迁移,分析阶段用 Polars,ML 阶段 to_pandas()
转换
存量 Pandas 代码 > 5000 行
新模块用 Polars,旧模块按需重构
6.3 渐进迁移策略
to_pandas() 在底层使用 Arrow 零拷贝机制,转换耗时在毫秒级,不构成性能瓶颈。
七、完整数据链路的性能优化
在企业场景中,性能瓶颈不仅在分析阶段,也在数据采集阶段。原始数据通过 API 或爬虫从外部获取时,采集效率直接影响分析的时效性。
```import requests
import polars as pl
from io import StringIO
通过亿牛云 API 代理获取出口 IP,避免单 IP 被限流
api_url = "http://ip.16yun.cn:817/myip/pl//?s=&u=&format=json"
proxy_info = requests.get(api_url, timeout=10).json()[0]
proxy = f"http://{proxy_info['ip']}:{proxy_info['port']}"
采集数据(返回 403 检查白名单,返回 429 降低频率)
resp = requests.get("https://api.example.com/export",
proxies={"http": proxy, "https": proxy}, timeout=30)
直接从内存字符串载入 Polars,跳过文件落盘
df = pl.read_csv(StringIO(resp.text), try_parse_dates=True)
Polars Lazy 链式分析
result = (df.lazy()
.filter(pl.col('amount') > 100)
.group_by('region')
.agg(pl.col('amount').sum().alias('revenue'))
.collect()
)
```
这条链路的优化点是:代理 IP 保证采集不被限流,read_csv(StringIO) 省去文件 I/O,Polars Lazy 引擎在 collect() 时才触发执行并做全局优化。相比"采集 → 存 CSV → Pandas 读取 → 分析"的传统链路,整体耗时减少 60% 以上。
八、最终判断
基于实测数据,迁移决策可以拆成三个层次:
第一层:看数据规模
数据量
推荐方案
< 10 万行
Pandas,迁移无意义
10 万~500 万行
Pandas + PyArrow 后端
500 万~5000 万行
Polars(单机最优解)
5000 万行
Polars Lazy + DuckDB
第二层:看瓶颈操作
如果业务中高频使用 Join 和 GroupBy,迁移收益最大——这两个操作 Polars 快 6~7 倍。如果主要是列选择和简单过滤,差距不到 3 倍,迁移必要性不大。
第三层:看生态依赖
重度依赖 scikit-learn、statsmodels、Matplotlib 的项目,最终仍需转回 Pandas。但 to_pandas() 的零拷贝转换耗时在毫秒级,不构成实质障碍。
"Polars 碾压 Pandas"这个说法在千万级数据 + 计算密集型操作的条件下成立,在十万级数据 + 简单过滤的场景下不成立。选型的正确姿势是:先用你的真实数据跑一遍 benchmark,再基于实测数字做决策,而不是被社区营销数字牵着走。