数据STUDIO

2025年了,还在用 Pandas 那就OUT 了!

Image

Image

Python生态系统以其易用性著称,Pandas作为其中最受欢迎的数据处理库,为初学者提供了友好的入门体验。然而,随着数据规模从MB级跃升至GB甚至TB级,Pandas的单线程执行模式、内存低效利用以及缺乏原生并行处理能力等问题日益凸显。三个关键概念在Pandas中几乎不存在却对大规模生产环境至关重要:首先是并行处理能力,现代CPU多核架构的潜力在Pandas中无法充分发挥;其次是查询优化,Pandas缺乏对执行计划的重写和优化;最后是惰性评估策略,导致不必要的中间结果计算和内存占用。

Pandas vs Polars: Performance Battle | by Venkatakrishnan | ILLUMINATION | Medium

Polars从根本上重新设计了数据处理引擎的架构,其核心优势源自几个关键设计决策。该库完全采用Rust编写,这种内存安全且无垃圾回收机制的语言为高性能提供了基础保障。多线程查询引擎能够自动将工作负载分布到所有可用的CPU核心,实现真正的并行计算。Polars还实现了基于表达式的查询优化器,可以重新排列操作顺序以减少内存占用和提高执行效率。值得一提的是,Polars提供了两种执行模式:即时执行的eager模式(类似Pandas体验)和构建完整查询计划后再执行的lazy模式,后者通过优化整个操作管道可进一步提升性能。

今天,你将看到 Polars 与 Pandas 在一个有 1,100 万行的 CSV 文件上进行的 4 个系列基准测试中的比较。

首先,来看看为什么要考虑用 Polars 来替代 Pandas。

Pandas 与 Polars 之争

为什么数据专家应该拥抱 Polars?

作为数据从业者,我们都经历过这样的时刻:看着Pandas处理数据时进度条缓慢前进,心里盘算着要不要先去喝杯咖啡。这就像开着一辆老式汽车在高速公路上行驶 - 它能带你到达目的地,但过程实在不够优雅。

Pandas确实是Python数据分析的绝佳入门选择,就像训练轮对学骑自行车的人一样有用。它的API直观易懂,文档丰富,社区支持强大。但当你的数据量从练习用的"玩具数据集"变成真实业务中的庞然大物时,问题就开始显现了。

Pandas的三大硬伤

  1. 单线程困境
    在这个多核处理器普及的时代,Pandas依然固执地使用单线程。就像让一个工人搬完整个仓库的货物,而其他工人却在旁边闲着。当数据集超过百万行时,这种设计决策的代价就变得极其明显。
  2. 内存暴食症
    Pandas要求将整个DataFrame加载到内存中。处理几个GB的数据?准备好你的内存条预算。更大的数据集?直接"内存不足"警告。这就像试图用家用冰箱存储超市的所有库存 - 根本不现实。
  3. 机械式执行
    Pandas会严格按照你写的代码顺序执行操作,即使这不是最优路径。就像严格按照食谱步骤做菜,明明可以同时煮面和炒酱,却非要等面煮好才开始炒酱。

Polars的四大超能力

  1. 并行处理怪兽
    Polars能自动利用所有可用的CPU核心,就像雇佣了整个工人团队而不是单打独斗。一个简单的groupby操作就能快上10倍?
  2. 内存管理大师
    借助Apache Arrow的内存格式和延迟执行,Polars的内存效率高得惊人。处理同样规模的数据,内存占用经常只有Pandas的一半。
  3. 智能查询优化器
    Polars会像经验丰富的厨师一样重新安排操作顺序。当你写出低效的查询时,它会悄悄帮你优化,就像有个数据工程师在幕后为你重写代码。
  4. 超越内存限制
    通过流式处理API,Polars能处理远超内存大小的数据集。想象一下不用升级硬件就能分析TB级数据的感觉。

迁移成本比你想的低

最令人欣慰的是,从Pandas迁移到Polars的学习曲线非常平缓。大多数常见操作都有对应的Polars实现,而且语法相似度很高。就像从iPhone换到Android旗舰机 - 核心体验相似,但性能提升立竿见影。

  • 语法相似度70%:很多操作直接把.df改成.pl就能用

    # Pandas
    df.groupby('城市')['销售额'].mean()

    # Polars
    df.groupby('城市').agg(pl.col('销售额').mean())
  • 无缝转换:可以和Pandas互相转化,不影响现有代码

    pandas_df = polars_df.to_pandas()
    polars_df = pl.from_pandas(pandas_df)
  • 支持SQL:直接用SQL查询数据

    df.sql("SELECT 城市, AVG(销售额) FROM self GROUP BY 城市")

真实案例:某电商平台将他们的RFM客户分析从Pandas迁移到Polars后,处理2000万用户数据的时间从47分钟缩短到4分钟 - 这不是理论上的性能提升,而是实实在在的生产力飞跃。

作为数据专家,选择工具不仅要考虑当下的便利性,更要着眼未来的可扩展性。当你的数据规模呈指数级增长时,Polars提供的性能优势可能就意味着能否在deadline前交付分析结果,或者能否实时响应业务需求。这不再是一个简单的技术选型问题,而是关乎工作效率和职业发展的战略决策。

基准前提--如何用 Python 创建大型数据集

首先,你需要一些数据。下面的代码段创建了一个随机数据框,其中包含 125 年(每小时间隔)和 10 个办公地点的虚构销售额。

这将为你提供大约 1100 万行供你使用:

import uuid
import numpy as np
import pandas as pd
import polars as pl
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("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("Finished constructing data!")
print("Converting to dataframe...")

data = pd.DataFrame(data)

print("Finished converting to dataframe!")
print("Saving to CSV...")

data.to_csv("data.csv", index=False)

print("Saved to CSV!")
print("Done!")

如果你想知道,UUID 字符串列的存在只是为了让数据集更大,读取速度更慢。

下面打印形状和数据样本,以验证一切看起来都很好:

print(data.shape)
print()
data.sample(10)

结果:

Image

看起来不错!你的数据集有 1104 万行,占用了大约 800MB 的磁盘空间。虽然不是很多,但足以看出 Pandas 和 Polars 之间是否存在显著差异。

如果需要更多数据,只需调整日期聚合频率即可。将小时换成分钟,就能获得超过 6.6 亿行的数据。

Pandas 与 Polars 数据处理基准对比

每个基准测试我都运行了3次,并取平均值。选择的硬件是一台 M2 MacBook Pro,16GB 内存和 12 个 CPU 内核。库版本如下

  • pandas-2.2.1
  • polars-0.20.18

Benchmark #1--读取 CSV 文件

几乎所有数据管道的首要任务都是获取数据。现在的文件格式是 CSV,Pandas 和 Polars 都有一个相同的 read_csv() 函数可供使用。

# PANDAS
df_pd = pd.read_csv("data.csv")


# POLARS
df_pl = pl.read_csv("data.csv")

应用程序接口可能是一样的,但幕后执行肯定不一样:

Image

从磁盘读取 800MB 的 CSV 文件时,Polars 的速度几乎是 Pandas 的 10 倍。

Benchmark #2 - 读取和预处理 CSV 文件

数据集读取后通常会立即进行数据预处理,如数据类型转换和过滤。这就是我要测试的用例。

我们的目标是再次读取 CSV 文件,将字符串日期列转换为实际的日期时间对象,只保留意大利办公室位置的行,并删除不必要的列。

Pandas 和 Polars 之间的实现初看起来有很多不同,但经验丰富的 Python 用户在两者之间应该没有问题:

# PANDAS
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"]]


# POLARS
df_pl_italy = (
    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"]))
)

Polars 的速度再次加快:

Image

现在只快了 5 倍,但仍然意义重大。

Benchmark #3 — Group By

分组是最常见的数据操作之一。其目的是浓缩信息,并显示不同分组的汇总统计数据,如总数和平均值。

下面的代码片段按年份和地点对数据进行分组,然后计算总销售额和平均销售额。生成的数据框按日期升序排序,按销售总额降序排序:

# PANDAS
(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])
)


# POLARS
(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])     
)

Pandas 和 Polars API 之间存在细微差别,主要体现在函数和参数名称上。但实际上,这并不是决定性因素。

Image

Polars 又快了一步,但差距并不大。分组操作本来就不需要太多时间,这就是主要原因。

Benchmark #4 - 完整管道

比较 Pandas 和 Polars 的单个操作是可以的,但大多数数据专家只关心数据管道完成所需的总时间。

这正是 Polars 的优势所在。它随附了一个快速评估数据帧版本,称为LazyFrames。它们背后的理念是建立一个计算图,让 Polars 优化执行顺序。

你不必动一根手指,但请注意你现在使用的是 scan_csv()函数,而不是 read_csv()。长话短说,这个函数创建了一个 LazyFrame,它会引用文件数据,而不是从磁盘读取数据。需要注意的是,你必须手动调用 collect()函数,因为在调用之前不会进行任何数据处理--因此是lazy:

# PANDAS
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])
)


# POLARS
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])
)


# POLARS WITH LAZY EVALUATION
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()

但额外的功能调用是值得的:

Image

与 Pandas 相比,Polars 在整个流程中的速度几乎快了 5 倍。如果使用其懒惰应用程序接口(lazy API),两者之间的差距将跃升至近 12 倍。

只需修改极少的代码,就能获得如此大的性能提升,令人印象深刻。

写在最后

与 Pandas 相比,使用 Polars 的速度快如闪电。今天展示的每项操作都更快,而且事实证明,从 Pandas 切换到懒散评估的 Polars,可以将整个数据处理管道的速度提高 12 倍。

既然如此,为什么大多数数据专家仍在使用Pandas呢?Pandas 可能是大多数人学会的第一个数据处理框架,而且人们对它的认知度也更高。随着时间的推移,改变只会越来越难。

Pandas 也是中小型数据集的最佳选择。但当数据量超过内存时,两者就不分伯仲了。Polars 胜出一筹。

你是否在工作区用 Polars 替代过 Pandas?请在评论区分享。

最后,推荐阅读:Polars (最强Pandas平替)

🏴‍☠️宝藏级🏴‍☠️ 原创公众号『数据STUDIO』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括可戳👉Python|MySQL|数据分析|数据可视化|机器学习与数据挖掘|爬虫等,从入门到进阶!

长按👇关注- 数据STUDIO -设为星标,干货速递ImageImage