ITPub博客

首页 > 数据库 > 数据库开发技术 > 再见pandas,你好polars!

再见pandas,你好polars!

原创 数据库开发技术 作者:jackzhang 时间:2024-04-12 06:30:00 0 删除 编辑
“

Tips:本文的代码我已整理成完整的脚本文件,需要的朋友请关注我的公众号并在后台回复关键字“pandas vs polars”免费获取(PS:请注意关键字一定要完全正确,否则获取不到资源)。

引言(Preface)

在一个按秒计算计算时间成本的世界中,尽可能地将其最小化变得至关重要。这在Python庞大的数据处理生态系统中尤为重要,随着数据集规模的增长,效率的提升成为了扩展性的关键。虽然Pandas对于初学者来说是一个很好的起点,但是当面对大规模生产环境时,要想克服其局限性,就需要更深入地理解诸如并行处理、查询优化和惰性评估等概念——而这些概念在Pandas中并没有得到很好的支持。

于是,Polars登场了。Polars以性能为核心设计,成为了一个从头开始专为速度而打造的Python库。拥有用Rust编写的多线程查询引擎,Polars承诺提供异常快速的数据处理速度,通常比Pandas快30到50倍。

今天,我们将通过一系列对包含超1100万行的CSV文件进行的四项基准测试,直接比较Polars与Pandas的性能。但在深入研究结果之前,让我们先探讨一下为什么Polars会成为Pandas的一个绝佳替代选项且令人信服的原因。

作为数据专业人员你为什么应该考虑Polars

  • 如果Python是你学习的第一种编程语言,我猜你的第一个数据处理框架可能是Pandas。对初学者来说,这并没有什么问题。但是一旦数据集变得很大,你就会遇到性能限制。

  • 其中最大的一个就是Pandas的单线程特性。数据集的大小每天都在增长,所以仅仅利用一个CPU核心并不合理。并行和/或分布式处理才是你的好帮手。

  • 更令人恼火的是,Pandas要求你将整个数据框存储在内存中,这在最理想的情况下也并不高效,而且要处理比内存大的数据集根本不可能。

  • 最后,Pandas并没有对操作顺序进行优化。这意味着数据处理操作将按照你声明它们的顺序应用,这很少是最优的。你并不一定知道最好的方法,但这没关系。

Polars登场

Polars是一个基于Python和Rust的库,用于快速处理大量数据。它是用Rust(一种具有C/C++性能的编程语言)编写的,提供了Pandas无法匹敌的数据处理优势:

  • 并行计算——您可以利用机器上所有可用的CPU核心。
  • 查询优化——通过使用查询规划和惰性评估,大幅减少了内存分配。你将在今天的其中一个基准测试中看到后者的实际效果。
  • 文件扫描——通过仅读取模式而不是将整个数据集存储在内存中,允许您处理大于内存的数据集。
  • 改进的写时复制——Polars使用Apache Arrow来提高存储性能。
  • ...(其他优势)。

从Pandas切换到Polars只需要进行最少的代码更改,因为它们的API非常相似。

现在你已经对Polars的优点有所了解。接下来我们一起来看看它的实际效果,所以我们直接开始吧!

Pandas VS. 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亿行。但是那将耗费更多的时间和内存空间,尤其是内存空间将将呈指数上升。所以,如果非必要我劝你别那么做。

Pandas VS. Polars——数据处理基准测试

处于对测试结果的严谨性和准确性考虑,每个基准测试我都运行了3次,然后求其平均值。此外,测试硬件环境、pandas和polars的版本如下:

  • 硬件环境——24G内存、6个CPU核心的Intel笔记本
  • pandas——2.2.1
  • polars——0.20.19

基准测试#1——CSV文件读取

几乎所有数据管道的第一个任务都是获取数据。当前的文件格式是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倍。

基准测试#2——CSV文件读取和数据预处理

在数据分析中,数据集读取之后通常紧接着就是数据预处理,例如数据类型转换和过滤。这就是我要在这里测试的用例。

目标是再次读取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倍,差异仍然显著。

基准测试#3——分组(Group By)

最常见的数据操作之一就是分组。其思想是压缩信息并显示汇总统计信息,例如按条件分组求和、平均值。

下面的代码片段按年份和位置对数据进行分组,然后计算总销售额和平均销售额。结果数据框按日期升序排序,同时按总销售额降序排序:

# 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倍),主要表现在函数和参数名称上。但实际上,这不应该是决定因素。其实主要原因是分组操作耗时较少。

基准测试#4——完整数据管道

比较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倍),差异显著。

结论(Conclusion)

与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啦,用这个让你的代码运行速度更快

Python向量化:让你的程序运行速度爆炸性增长

Python探索性数据分析之数据可视化:附实例代码



原文作者:数据派探险家;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-12-19

  • 博文量
    119
  • 访问量
    55993