我在Stack Overflow上看到过许多涉及使用Pandas方法的问题的答案。我还看到用户在它们下面评论说“应用很慢,应该避免”。

我读过很多关于性能的文章,解释应用速度很慢。我还在文档中看到了一个免责声明,关于apply如何只是一个传递udf的方便函数(现在似乎找不到)。因此,普遍的共识是,如果可能的话,应该避免使用apply。然而,这引发了以下问题:

如果apply是如此糟糕,那么为什么它在API中? 我应该如何以及何时使我的代码无应用程序? 是否存在任何情况下apply是好的(比其他可能的解决方案更好)?


应用,你永远不需要的便利功能

我们从解决OP中的问题开始,一个接一个。

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

DataFrame。应用和系列。apply是分别定义在DataFrame和Series对象上的方便函数。apply接受任何用户定义的对DataFrame应用转换/聚合的函数。Apply是一个有效的银弹,可以完成任何现有pandas函数无法完成的任务。

适用的一些事情可以做到:

在DataFrame或Series上运行任何用户定义的函数 在数据帧上按行(轴=1)或按列(轴=0)应用函数 在应用函数时执行索引对齐 使用用户定义的函数执行聚合(但是,在这些情况下,我们通常更喜欢agg或transform) 执行元素转换 将聚合结果广播到原始行(请参阅result_type参数)。 接受位置/关键字参数传递给用户定义的函数。

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

那么,有了所有这些功能,为什么apply不好呢?这是因为应用很慢。Pandas对函数的性质没有任何假设,因此在必要时迭代地将函数应用到每一行/列。此外,处理上述所有情况意味着应用在每次迭代中都会引起一些主要的开销。此外,apply会消耗更多的内存,这对内存有限的应用程序是一个挑战。

很少有适合使用apply的情况(下文将详细介绍)。如果您不确定是否应该使用apply,则可能不应该使用。



让我们讨论下一个问题。

“我应该如何以及何时使我的代码无应用程序?”

换句话说,这里有一些常见的情况,在这些情况下,您可能希望摆脱要应用的任何调用。

数值型数据

如果你正在处理数字数据,很可能已经有一个向量化的cython函数,它完全可以做你想做的事情(如果没有,请在Stack Overflow上问一个问题,或者在GitHub上打开一个功能请求)。

对比应用程序对简单加法操作的性能。

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

性能方面,没有可比性,胞化的等效物要快得多。不需要图表,因为即使是玩具数据,差异也很明显。

%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”分别出现在它们的“Title”列中。

使用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)

这里需要注意的是,迭代例程碰巧比应用程序快,因为开销更低。如果需要处理nan和无效dtype,可以在此基础上使用自定义函数,然后在列表理解式中使用参数调用该函数。

有关何时应该考虑列表理解是一个好选择的更多信息,请参阅我的文章:熊猫中的For循环真的很糟糕吗?我什么时候该在乎?

请注意 Date和datetime操作也有向量化的版本。因此,例如,您应该使用pd.to_datetime(df['date']),而不是, 说,df(“日期”)苹果(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是一个方便函数,因此在某些情况下,开销可以忽略不计。它实际上取决于函数被调用了多少次。

系列向量化的函数,而不是数据框架 如果想对多个列应用字符串操作怎么办?如果要将多个列转换为datetime,该怎么办?这些函数仅针对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
    

这是一个可接受的情况,适用于:

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

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

注意,堆栈也有意义,或者只是使用显式循环。所有这些选项都比使用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))

v/s

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)

等等……


将Series转换为str: astype与apply

这似乎是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())

使用float,我看到astype始终与apply一样快,或略快于apply。这与测试中的数据是整数类型有关。


具有链式转换的GroupBy操作

GroupBy。apply到现在还没有讨论,但是GroupBy。apply也是一个迭代的方便函数,可以处理现有GroupBy函数不能处理的任何事情。

一个常见的需求是执行GroupBy,然后执行两个素数操作,例如“滞后cumsum”:

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

很难量化绩效,因为它取决于数据。但是一般来说,如果目标是减少groupby调用,apply是一个可以接受的解决方案(因为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

这种行为在GroupBy中也可以看到。适用于<0.25的pandas版本(它被固定为0.25,参见这里了解更多信息)。


并不是所有的申请都一样

下面的图表说明了何时考虑apply1。绿色表示可能有效;红色的避免。

有些是直观的:pd.Series.apply是python级别的逐行循环,同样是pd. datafframe .apply逐行循环(axis=1)。滥用这些工具的情况很多,范围也很广。另一篇文章更深入地讨论了它们。流行的解决方案是使用向量化方法、列表推导(假设数据干净)或有效的工具,如pd。DataFrame构造函数(例如避免应用(pd.Series))。

如果你在逐行使用pd. datafframe .apply,指定raw=True(如果可能的话)通常是有益的。在这个阶段,numba通常是更好的选择。

GroupBy。适用范围:一般青睐

重复groupby操作以避免apply将损害性能。GroupBy。Apply在这里通常很好,前提是您在自定义函数中使用的方法本身是向量化的。有时,您希望应用的分组聚合没有本地Pandas方法。在这种情况下,对于少量的组应用,使用自定义功能仍然可以提供合理的性能。

按列应用:混合包

pd. datafframe .apply列(轴=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

有例外,但这些通常是边缘或不常见的。举几个例子:

Df ['col'].apply(str)可能略优于Df ['col'].astype(str)。 Df.apply (pd.to_datetime)处理字符串与常规for循环相比不能很好地扩展行。


有没有什么情况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列表理解。


对于axis=1(即行函数),您可以使用下面的函数来代替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)