我什么时候应该(不)想在我的代码中使用 pandas apply()?

2024-11-19 08:38:00
admin
原创
12
摘要:问题描述:我在 Stack Overflow 上看到过很多关于使用 Pandas 方法的回答apply,也有用户在回答下评论说“apply很慢,应该避免”。我读过很多关于性能的文章,都说 explainapply很慢。我还在文档中看到了一个免责声明,说明 howapply只是传递 UDF 的便利函数(现在似乎...

问题描述:

我在 Stack Overflow 上看到过很多关于使用 Pandas 方法的回答apply,也有用户在回答下评论说“apply很慢,应该避免”。

我读过很多关于性能的文章,都说 explainapply很慢。我还在文档中看到了一个免责声明,说明 howapply只是传递 UDF 的便利函数(现在似乎找不到)。因此,普遍的共识是,apply如果可能的话,应该避免这种情况。然而,这引发了以下问题:

  1. 如果apply这么糟糕,那它为什么在 API 中?

  2. 我应该如何以及何时使我的代码apply无代码?

  3. apply是否存在好的情形(比其他可能解决方案更好)?


解决方案 1:

apply,您从未需要的便捷功能

我们首先逐一解答 OP 中的问题。

“如果apply这么糟糕,那它为什么会出现在 API 中?”

DataFrame.apply和分别Series.apply是在 DataFrame 和 Series 对象上定义的便捷函数apply。接受任何在 DataFrame 上应用转换/聚合的用户定义函数。apply实际上是一种银弹,可以做任何现有的 pandas 函数无法做到的事情。

可以做的一些事情apply

  • 在 DataFrame 或 Series 上运行任何用户定义的函数

  • 在 DataFrame 上按行 ( axis=1) 或按列 ( ) 应用函数axis=0

  • 在应用函数时执行索引对齐

  • 使用用户定义的函数执行聚合(然而,在这些情况下,我们通常更喜欢agg或)transform

  • 执行元素级转换

  • 将聚合结果广播到原始行(参见result_type参数)。

  • 接受位置/关键字参数传递给用户定义的函数。

...等等。有关更多信息,请参阅文档中的行或列函数应用。

那么,有了这些功能,为什么apply它不好呢?因为apply 很慢。Pandas 不会对函数的性质做出任何假设,因此会根据需要将函数迭代应用于每一行/列。此外,处理上述所有情况意味着apply每次迭代都会产生一些重大开销。此外,apply它会消耗更多内存,这对内存受限的应用程序来说是一个挑战。

适合使用的情况非常少apply(详见下文)。如果你不确定是否应该使用apply,那么最好不要使用。



pandas 2.2 更新:apply现在支持engine='numba'

更多信息请参阅发行说明以及GH54666

在应用中选择 python(默认)引擎或 numba 引擎。

numba 引擎将尝试对传递的函数进行 JIT 编译,这可能会加快大型 DataFrame 的速度。它还支持以下 engine_kwargs :

  • nopython(以nopython模式编译函数)

  • nogil(释放JIT编译函数内的GIL)

  • 并行(尝试在 DataFrame 上并行应用该函数)

注意:由于 numba/pandas 与 numba 接口方式的限制,您只能在 raw=True 时使用此功能


让我们讨论下一个问题。

“我应该如何以及何时使我的代码apply无害?”

换句话说,这里有一些常见的情况,在这些情况下您会想要摆脱对 的任何调用apply

数值数据

如果您正在处理数字数据,那么很可能已经有一个矢量化的 cython 函数可以完成您想要做的事情(如果没有,请在 Stack Overflow 上提问或在 GitHub 上提出功能请求)。

apply对比简单加法运算的性能。

df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df

   A   B
0  9  12
1  4   7
2  2   5
3  1   4

<!- ->

df.apply(np.sum)

A    16
B    28
dtype: int64

df.sum()

A    16
B    28
dtype: int64

性能方面,没有可比性,cython 化版本速度更快。无需图表,因为即使是玩具数据,差异也很明显。

%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

即使你启用通过raw参数传递原始数组,速度仍然会慢两倍。

%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

另一个例子:

df.apply(lambda x: x.max() - x.min())

A    8
B    8
dtype: int64

df.max() - df.min()

A    8
B    8
dtype: int64

%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()

2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

一般来说,如果可能的话,寻找矢量化的替代方案。


字符串/正则表达式

在大多数情况下,Pandas 都提供“矢量化”字符串函数,但在极少数情况下,这些函数不……可以说是“适用”。

一个常见的问题是检查某一列中的值是否存在于同一行的另一列中。

df = pd.DataFrame({
    'Name': ['mickey', 'donald', 'minnie'],
    'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
    'Value': [20, 10, 86]})
df

     Name  Value                       Title
0  mickey     20                  wonderland
1  donald     10  welcome to donald's castle
2  minnie     86      Minnie mouse clubhouse

这应该返回第二行和第三行,因为“donald”和“minnie”出现在它们各自的“标题”列中。

使用 apply,可以使用

df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)

0    False
1     True
2     True
dtype: bool
 
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

然而,使用列表推导存在更好的解决方案。

df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

<!- ->

%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

这里要注意的是,迭代例程恰好比 更快apply,因为开销较低。如果您需要处理 NaN 和无效 dtype,则可以在此基础上使用自定义函数进行构建,然后可以使用列表推导中的参数进行调用。

有关何时应将列表推导视为良好选择的更多信息,请参阅我的文章:Pandas 中的 for 循环真的很糟糕吗?我什么时候应该关心它?。

注意

日期和日期时间操作也有矢量化版本。因此,例如,您应该pd.to_datetime(df['date'])更喜欢 ,而不是df['date'].apply(pd.to_datetime)

更多内容请阅读
文档。


常见陷阱:列表列爆炸

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

人们倾向于使用。从性能方面来看,apply(pd.Series)这很糟糕。

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

更好的选择是列出该列并将其传递给 pd.DataFrame。

pd.DataFrame(s.tolist())

   0  1
0  1  2
1  1  2
2  1  2

<!- ->

%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())

2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)


最后,

“有没有什么情况是apply好的?”

Apply 是一个便利函数,因此在某些情况下,开销可以忽略不计。这实际上取决于调用该函数的次数。

针对 Series 进行矢量化的函数,但不是针对 DataFrames 的函数

如果想对多列应用字符串操作怎么办?如果想将多列转换为日期时间怎么办?这些函数仅针对 Series 进行矢量化,因此必须将它们应用于要转换/操作的每一列。

df = pd.DataFrame(
         pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), 
         columns=['date1', 'date2'])
df

       date1      date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30

df.dtypes

date1    object
date2    object
dtype: object
    

这是一个可以接受的案例apply

df.apply(pd.to_datetime, errors='coerce').dtypes

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object

stack请注意, 或仅使用显式循环也是有意义的。所有这些选项都比使用 稍快apply,但差异很小,可以原谅。

%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')

5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

您可以对其他操作(例如字符串操作或转换为类别)进行类似的操作。

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))

速度

u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
    v[c] = df[c].astype(category)

等等...


将系列转换为strastypeapply

这似乎是 API 的一个特性。使用apply将 Series 中的整数转换为字符串与使用 相当(有时甚至更快)astype

在此处输入图片描述
该图是使用该perfplot库绘制的。

import perfplot

perfplot.show(
    setup=lambda n: pd.Series(np.random.randint(0, n, n)),
    kernels=[
        lambda s: s.astype(str),
        lambda s: s.apply(str)
    ],
    labels=['astype', 'apply'],
    n_range=[2**k for k in range(1, 20)],
    xlabel='N',
    logx=True,
    logy=True,
    equality_check=lambda x, y: (x == y).all())

对于浮点数,我发现astype始终与 一样快,或者稍快一些apply。所以这与测试中的数据是整数类型有关。


GroupBy链式变换操作

GroupBy.apply到目前为止还没有讨论过,但GroupBy.apply它也是一个迭代便利函数,可以处理现有GroupBy函数无法处理的任何事情。

一个常见的要求是执行 GroupBy,然后执行两个主要运算,例如“滞后累计”:

df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df

   A   B
0  a  12
1  a   7
2  b   5
3  c   4
4  c   5
5  c   4
6  d   3
7  d   2
8  e   1
9  e  10

<!- ->

这里您需要两次连续的 groupby 调用:

df.groupby('A').B.cumsum().groupby(df.A).shift()
 
0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

使用apply,您可以将其缩短为单个调用。

df.groupby('A').B.apply(lambda x: x.cumsum().shift())

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

很难量化性能,因为这取决于数据。但一般来说,apply如果目标是减少groupby调用次数,这是一个可以接受的解决方案(因为groupby成本也相当高)。



其他注意事项

除了上述注意事项外,还值得一提的是,它对apply第一行(或第一列)进行两次操作。这样做是为了确定该函数是否有任何副作用。如果没有,apply则可以使用快速路径来评估结果,否则它会退回到缓慢的实现。

df = pd.DataFrame({
    'A': [1, 2],
    'B': ['x', 'y']
})

def func(x):
    print(x['A'])
    return x

df.apply(func, axis=1)

# 1
# 1
# 2
   A  B
0  1  x
1  2  y

这种行为在 pandas 版本 <0.25 中也能看到GroupBy.apply(在 0.25 中已经修复,更多信息请参见此处。)

解决方案 2:

并非所有的apply情况都一样

下图建议何时考虑apply1。绿色表示可能有效;红色表示避免。

在此处输入图片描述

其中一些是直观的:pd.Series.apply是 Python 级别的逐行循环,同上pd.DataFrame.apply逐行(axis=1)。这些的误用很多,范围很广。另一篇文章更深入地讨论了它们。流行的解决方案是使用矢量化方法、列表理解(假设数据干净)或高效工具pd.DataFrame(例如避免apply(pd.Series))。

如果您使用pd.DataFrame.apply按行方式,则指定raw=True(在可能的情况下)通常是有益的。在此阶段,numba通常是更好的选择。

GroupBy.apply:普遍受到青睐

groupby避免重复操作apply会损害性能。GroupBy.apply通常情况下,这样做是没问题的,只要您在自定义函数中使用的方法本身是矢量化的。有时,对于您希望应用的分组聚合,没有原生的 Pandas 方法。在这种情况下,对于少数组,apply使用自定义函数可能仍会提供合理的性能。

pd.DataFrame.apply按栏目分类:好坏参半

pd.DataFrame.apply按列 ( axis=0) 是一个有趣的案例。对于行数较少而列数较多的情况,它几乎总是很昂贵。对于行数相对于列数较多的情况(更常见的情况),有时您可能会看到使用 可以显著提高性能apply

# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns

                                               # Scenario_1  | Scenario_2
%timeit df.sum()                               # 800 ms      | 109 ms
%timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms

%timeit df.max() - df.min()                    # 1.63 s      | 314 ms
%timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms

%timeit df.mean()                              # 108 ms      | 94.4 ms
%timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms

1也有例外,但这些情况通常很少见或不常见。以下是一些例子:

  1. df['col'].apply(str)可能会略微跑赢df['col'].astype(str)

  2. df.apply(pd.to_datetime)与常规循环相比,对字符串的处理不能很好地与行一起进行for

解决方案 3:

对于axis=1(即逐行函数),您可以使用以下函数代替apply。我想知道为什么这不是pandas行为。(未经复合索引测试,但它确实看起来比快得多apply

def faster_df_apply(df, func):
    cols = list(df.columns)
    data, index = [], []
    for row in df.itertuples(index=True):
        row_dict = {f:v for f,v in zip(cols, row[1:])}
        data.append(func(row_dict))
        index.append(row[0])
    return pd.Series(data, index=index)

解决方案 4:

有没有apply好的情况?是的,有时候。

任务:解码 Unicode 字符串。

import numpy as np
import pandas as pd
import unidecode

s = pd.Series(['mañana','Ceñía'])
s.head()
0    mañana
1     Ceñía


s.apply(unidecode.unidecode)
0    manana
1     Cenia

更新

我绝不提倡使用apply,只是认为既然NumPy不能处理上述情况,它可能是 的一个很好的候选者pandas apply。但由于 @jpp 的提醒,我忘记了普通的 ol 列表理解。

解决方案 5:

我想补充一下不使用的原因,apply其他答案中没有提到。apply可能会导致严重的突变错误,即df.apply(func, axis=1)对给定函数应用相同的对象。试试这个:

import pandas as pd

df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
print(df.apply(id, axis=1))
# 0    2771319967472
# 1    2771319967472
# 2    2771319967472
# 3    2771319967472

id所有值都相同)

申请文档指出:

改变传递对象的函数可能会产生意外行为或错误,因此不受支持。

但即使知道这一点并避免突变也还不够。考虑以下代码:

from asyncio import run, gather, sleep
import pandas as pd


async def afunc(row):
    await sleep(1)
    print(row)


async def main():
    df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
    coros = df.apply(afunc, axis=1)
    await gather(*coros)


run(main())

如您所见,我们没有改变任何行afunc。但是如果你运行代码,你会注意到只有最后一行被应用了……

另一个没有的例子async

import pandas as pd

applied = []


def func(row):
    applied.append(row)


df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df.apply(func, axis=1)
print(pd.DataFrame(applied))
#    A  B
# 3  1  4
# 3  1  4
# 3  1  4
# 3  1  4

替代解决方案

df.to_records()`df.itertuples()`甚至比处理整个帧还要快。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用