首页 > 数据库 > 数据库开发技术 > 再见pandas,你好polars!
“Tips:本文的代码我已整理成完整的脚本文件,需要的朋友请关注我的公众号并在后台回复关键字“pandas vs polars”免费获取(PS:请注意关键字一定要完全正确,否则获取不到资源)。
在一个按秒计算计算时间成本的世界中,尽可能地将其最小化变得至关重要。这在Python庞大的数据处理生态系统中尤为重要,随着数据集规模的增长,效率的提升成为了扩展性的关键。虽然Pandas对于初学者来说是一个很好的起点,但是当面对大规模生产环境时,要想克服其局限性,就需要更深入地理解诸如并行处理、查询优化和惰性评估等概念——而这些概念在Pandas中并没有得到很好的支持。
于是,Polars登场了。Polars以性能为核心设计,成为了一个从头开始专为速度而打造的Python库。拥有用Rust编写的多线程查询引擎,Polars承诺提供异常快速的数据处理速度,通常比Pandas快30到50倍。
今天,我们将通过一系列对包含超1100万行的CSV文件进行的四项基准测试,直接比较Polars与Pandas的性能。但在深入研究结果之前,让我们先探讨一下为什么Polars会成为Pandas的一个绝佳替代选项且令人信服的原因。
如果Python是你学习的第一种编程语言,我猜你的第一个数据处理框架可能是Pandas。对初学者来说,这并没有什么问题。但是一旦数据集变得很大,你就会遇到性能限制。
其中最大的一个就是Pandas的单线程特性。数据集的大小每天都在增长,所以仅仅利用一个CPU核心并不合理。并行和/或分布式处理才是你的好帮手。
更令人恼火的是,Pandas要求你将整个数据框存储在内存中,这在最理想的情况下也并不高效,而且要处理比内存大的数据集根本不可能。
最后,Pandas并没有对操作顺序进行优化。这意味着数据处理操作将按照你声明它们的顺序应用,这很少是最优的。你并不一定知道最好的方法,但这没关系。
Polars是一个基于Python和Rust的库,用于快速处理大量数据。它是用Rust(一种具有C/C++性能的编程语言)编写的,提供了Pandas无法匹敌的数据处理优势:
从Pandas切换到Polars只需要进行最少的代码更改,因为它们的API非常相似。
现在你已经对Polars的优点有所了解。接下来我们一起来看看它的实际效果,所以我们直接开始吧!
在做基准测试前,需要一个规模较大的数据集。在Python中,如何创建大型数据集呢?下面的代码片段创建了一个随机数据集,其中包含125年(以小时为间隔)和10个办公地点的虚构销售额,总行数超1100万行:
import uuid
import numpy as np
import pandas as pd
from datetime import datetime
date_ranges = pd.date_range(start=datetime(1900, 1, 1), end=datetime(2025, 12, 31), freq='h')
locations = ["Italy", "France", "Spain", "Portugal", "Greece", "Germany", "Austria", "Norway", "Sweeden", "Poland"]
location_multiplier = [0.7, 1.2, 0.9, 0.5, 0.3, 1.0, 1.4, 1.9, 2.1, 1.4]
data = []
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Started constructing data...")
for dt in date_ranges:
for i, loc in enumerate(locations):
data.append({
"id": str(uuid.uuid4()),
"date": dt,
"location": loc,
"sales": np.random.randint(low=1, high=50) * location_multiplier[i]
})
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Finished constructing data!")
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Converting data to dataframe...")
data = pd.DataFrame(data)
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Finished converting to dataframe!")
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Saving to CSV...")
data.to_csv("data.csv", index=False)
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Saved to CSV!")
print("Done!")
你可能会好奇uuid的含义,它是Python的一个内置模块,用于生成通用唯一标识符,这里用于生成id列的随机且唯一值,只是为了使数据集更大,读取速度更慢,从而更好的演示测试效果。
我们可以打印该数据集的大小和样本:
print(data.shape)
print()
data.sample(10)
看起来不错!我们得到了一个1104万行的数据集,占用了大约800MB的磁盘空间。不是很多,但应该足以看出Pandas和Polars之间是否有显著差异。
如果你需要更多数据,只需调整日期聚合频率即可。例如,将小时转换为分钟将获得超过6.6亿行。但是那将耗费更多的时间和内存空间,尤其是内存空间将将呈指数上升。所以,如果非必要我劝你别那么做。
处于对测试结果的严谨性和准确性考虑,每个基准测试我都运行了3次,然后求其平均值。此外,测试硬件环境、pandas和polars的版本如下:
几乎所有数据管道的第一个任务都是获取数据。当前的文件格式是CSV, Pandas和polar都可以使用一个相同的read_csv()函数。
import time
import polars as pl
# pandas
total_time_taken_pandas = []
for i in range(3):
start_time_pandas = time.time()
df_pd = pd.read_csv("data.csv")
end_time_pandas = time.time()
total_time_taken_pandas.append(end_time_pandas - start_time_pandas)
time_taken_pandas = round(sum(total_time_taken_pandas) / len(total_time_taken_pandas), 2)
# polars
total_time_taken_polars = []
for i in range(3):
start_time_polars = time.time()
df_pl = pl.read_csv("data.csv")
end_time_polars = time.time()
total_time_taken_polars.append(end_time_polars - start_time_polars)
time_taken_polars = round(sum(total_time_taken_polars) / len(total_time_taken_polars), 2)
print(f"Time taken by pandas: {time_taken_pandas}s")
print(f"Time taken by polars: {time_taken_polars}s")
print(f"Polars is {round(time_taken_pandas/time_taken_polars, 2)}X faster than Pandas")
从测试结果可以看出,从磁盘读取一个大约800MB的CSV文件,Polars的速度几乎是Pandas的17倍。
在数据分析中,数据集读取之后通常紧接着就是数据预处理,例如数据类型转换和过滤。这就是我要在这里测试的用例。
目标是再次读取CSV文件,将字符串日期列表示形式转换为实际的datetime对象,仅保留意大利办公室位置的行,并删除不必要的列。
Pandas和polar的实现起初看起来有很大不同,但经验丰富的Python人员在两者之间熟练切换应该没有问题:
# Reading and prepocessing a CSV file
# pandas
total_time_taken_pandas = []
for i in range(3):
start_time_pandas = time.time()
df_pd_italy = pd.read_csv("data.csv")
df_pd_italy["date"] = pd.to_datetime(df_pd_italy["date"])
df_pd_italy = df_pd_italy[df_pd_italy["location"] == "Italy"]
df_pd_italy = df_pd_italy[["date", "sales"]]
end_time_pandas = time.time()
total_time_taken_pandas.append(end_time_pandas - start_time_pandas)
time_taken_pandas = round(sum(total_time_taken_pandas) / len(total_time_taken_pandas), 2)
# polars
total_time_taken_polars = []
for i in range(3):
start_time_polars = time.time()
df_pl_itatly = (
pl.read_csv("data.csv")
.with_columns(pl.col("date").str.to_date("%Y-%m-%d %H:%M:%S"))
.filter(pl.col("location") == "Italy")
.select(pl.col(["date", "sales"]))
)
end_time_polars = time.time()
total_time_taken_polars.append(end_time_polars - start_time_polars)
time_taken_polars = round(sum(total_time_taken_polars) / len(total_time_taken_polars), 2)
print(f"Time taken by pandas: {time_taken_pandas}s")
print(f"Time taken by polars: {time_taken_polars}s")
print(f"Polars is {round(time_taken_pandas/time_taken_polars, 2)}X faster than Pandas")
文件读取加上数据预处理,Polars的速度是Pandas的9倍,差异仍然显著。
最常见的数据操作之一就是分组。其思想是压缩信息并显示汇总统计信息,例如按条件分组求和、平均值。
下面的代码片段按年份和位置对数据进行分组,然后计算总销售额和平均销售额。结果数据框按日期升序排序,同时按总销售额降序排序:
# Group by
# pandas
total_time_taken_pandas = []
for i in range(3):
df_pd = pd.read_csv("data.csv")
df_pd["date"] = pd.to_datetime(df_pd["date"])
start_time_pandas = time.time()
(df_pd.groupby([df_pd["date"].dt.year, df_pd["location"]])
.agg(total_sales=("sales", "sum"), avg_sales=("sales", "mean"))
.reset_index()
.sort_values(by=["date", "total_sales"], ascending=[True, False])
)
end_time_pandas = time.time()
total_time_taken_pandas.append(end_time_pandas - start_time_pandas)
time_taken_pandas = round(sum(total_time_taken_pandas) / len(total_time_taken_pandas), 2)
# polars
total_time_taken_polars = []
for i in range(3):
df_pl = pl.read_csv("data.csv")
df_pl = df_pl.with_columns(pl.col("date").str.to_date("%Y-%m-%d %H:%M:%S"))
start_time_polars = time.time()
(df_pl.group_by([pl.col("date").dt.year(), pl.col("location")])
.agg(pl.sum("sales").alias("total_sales"), pl.mean("sales").alias("avg_sales"))
.sort(by=["date", "total_sales"], descending=[False, True])
)
end_time_polars = time.time()
total_time_taken_polars.append(end_time_polars - start_time_polars)
time_taken_polars = round(sum(total_time_taken_polars) / len(total_time_taken_polars), 2)
print(f"Time taken by pandas: {time_taken_pandas}s")
print(f"Time taken by polars: {time_taken_polars}s")
print(f"Polars is {round(time_taken_pandas/time_taken_polars, 2)}X faster than Pandas")
从测试结果来看,对于分组操作,Pandas和polar之间的性能差异较小,但是Polars仍然更快(大致是Pandas的2倍),主要表现在函数和参数名称上。但实际上,这不应该是决定因素。其实主要原因是分组操作耗时较少。
比较Pandas和Polars的单个操作是可以的,但大多数数据专业人士只关心他们的数据管道完成所需的总时间。
这就是Polars的额外优势所在。它提供了一种称为LazyFrames的惰性评估数据框版本。其背后的思想是构建一个计算图,并让Polars优化执行顺序。
对你来说没有什么费力气的活,但请注意,现在你使用的是scan_csv()函数而不是read_csv()函数。长话短说,这个函数创建了一个LazyFrame,它引用文件的数据而不是从磁盘读取。其中需要注意的是你必须手动调用collect()函数,因为在你调用之前不会发生任何数据处理——因此我们称它是惰性的:
# The Complete Pipeline
# Pandas
total_time_taken_pandas = []
for i in range(3):
start_time_pandas = time.time()
df_pd = pd.read_csv("data.csv")
df_pd["date"] = pd.to_datetime(df_pd["date"])
df_pd_res = (
df_pd.groupby([df_pd["date"].dt.year,df_pd["location"]])
.agg(total_sales=("sales", "sum"), avg_sales=("sales", "mean"))
.reset_index()
.sort_values(by=["date", "total_sales"], ascending=[True, False])
)
end_time_pandas = time.time()
total_time_taken_pandas.append(end_time_pandas - start_time_pandas)
time_taken_pandas = round(sum(total_time_taken_pandas) / len(total_time_taken_pandas), 2)
# Polars
total_time_taken_polars = []
for i in range(3):
start_time_polars = time.time()
df_pl_res = (
pl.read_csv("data.csv")
.with_columns(pl.col("date").str.to_date("%Y-%m-%d %H:%M:%S"))
.group_by([pl.col("date").dt.year(), pl.col("location")])
.agg(pl.sum("sales").alias("total_sales"), pl.mean("sales").alias("avg_sales"))
.sort(by=["date", "total_sales"], descending=[False, True])
)
end_time_polars = time.time()
total_time_taken_polars.append(end_time_polars - start_time_polars)
time_taken_polars = round(sum(total_time_taken_polars) / len(total_time_taken_polars), 2)
# Polars with lazy evaluation
total_time_taken_polars_lazy = []
for i in range(3):
start_time_polars_lazy = time.time()
df_pl_lazy_query = (
pl.scan_csv("data.csv")
.with_columns(pl.col("date").str.to_date("%Y-%m-%d %H:%M:%S"))
.group_by([pl.col("date").dt.year(), pl.col("location")])
.agg(pl.sum("sales").alias("total_sales"), pl.mean("sales").alias("avg_sales"))
.sort(by=["date", "total_sales"], descending=[False, True])
)
df_pl_lazy_res = df_pl_lazy_query.collect()
end_time_polars_lazy = time.time()
total_time_taken_polars_lazy.append(end_time_polars_lazy - start_time_polars_lazy)
time_taken_polars_lazy = round(sum(total_time_taken_polars_lazy) / len(total_time_taken_polars_lazy), 2)
print(f"Time taken pandas: {time_taken_pandas}s")
print(f"Time taken polars: {time_taken_polars}s")
print(f"Time taken polars with lazy evaluation: {time_taken_polars_lazy}s")
print(f"Polars is {round(time_taken_pandas/time_taken_polars, 2)}X faster than Pandas")
print(f"Polars with lazy evaluation is {round(time_taken_pandas/time_taken_polars_lazy, 2)}X faster than Pandas")
对于完整数据管道,与Pandas相比,Polars快了7倍;如果换成是Polars的惰性版本,性能会更好些(8倍),差异显著。
与Pandas相比,使用Polars的处理速度快得惊人。今天展示的每个操作都更快,并且事实证明,通过从Pandas切换到惰性评估的Polars,可以将整个数据处理管道的速度提高8倍。单独的文件读取的话,Polars甚至比Pandas快17倍。
如果事实如此,那么为什么大多数数据专业人士仍然在使用Pandas呢?其实,归根结底,主要是因为Pandas的熟悉度和更广泛的生态系统。Pandas可能是大多数人学习的第一个数据处理框架,并且对它的认识更广泛。随着时间的推移,改变只会变得更加困难,同时也可能是Pandas已经用习惯的缘故。
对于小型到中等规模的数据集,Pandas确实也是一个绝佳的选择。但是当数据大于内存时,它根本无法与Polars相提并论。
“如果你觉得这篇文章对你有用,请“点赞➕关注”支持我(关注我,不迷路🤭🤭🤭),你的点赞和关注就是我持续创作分享的动力,让更多的朋友发现并加入我们,共同学习和进步!
Thank you for reading!
- END -
推荐阅读👇👇👇
Python中的return和yield关键字的用法和区别你知道吗?
Python小技巧:连接字符串不要再使用+号了,用这个方法更快
Python小技巧:循环不要再使用loc/iloc啦,用这个让你的代码运行速度更快
原文作者:数据派探险家;如有侵权,请联系管理员删除。