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

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

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

  1. 如果apply如此糟糕,那么为什么它会出现在API中?
  2. 我应该如何以及何时使我的代码_ABC_无0?
  3. 是否存在apply好的(优于其他可能的解决方案)的情况?
46442 次浏览

apply,您永远不需要的便利功能

我们从解决行动计划中的问题开始,一个接一个。

";如果apply是如此糟糕,那么为什么它在API中?";

DataFrame.applySeries.apply是分别在DataFrame和Series对象上定义的便利功能apply接受在DataFrame上应用转换/聚合的任何用户定义函数。apply是有效的银弹,它执行任何现有Pandas函数无法执行的操作。

apply可以执行的一些操作:

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

...除其他外。有关详细信息,请参阅文档中的“按行或按列函数应用程序 ”。

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

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



让我们解决下一个问题。

";我应该如何以及何时使我的代码_ABC_无0?";

以下是一些常见的情况,在这些情况下,您需要将任何呼叫的ABC_1_到apply

数字数据

如果您正在处理数字数据,很可能已经有一个矢量化的Cython函数,它可以执行您正在尝试执行的操作(如果没有,请询问有关堆栈溢出的问题或在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

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

%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";还有“米妮”出现在它们各自的“标题”中列。

使用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和无效的数据类型,您可以在此基础上使用自定义函数,然后您可以使用列表理解中的参数调用该函数。

关于列表理解何时应该被认为是一个好的选择的更多信息,请参阅我的文章:熊猫的循环真的很糟糕吗?我什么时候应该关心?

注意
日期和日期时间操作也有矢量化版本。例如,您应该选择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)


最后,

";是否存在_ABC_为0的情况?";

Apply是一个方便的函数,因此在的情况下,开销可以忽略不计,足以原谅。这实际上取决于函数被调用了多少次。

为系列而不是数据帧进行矢量化的函数
如果要对多个列应用字符串操作,该怎么办?如果要将多个列转换为DateTime,该怎么办?这些函数仅针对系列进行了矢量化,因此它们必须在要转换/操作的每一列上已应用

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将系列中的整数转换为字符串与使用astype相当(有时更快)。

enter image description here 使用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一样快,或者比后者稍快。所以这与测试中的数据是整型有关。


_具有链式转换的ABC_0操作

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

很难量化性能,因为它依赖于数据。但是通常,如果目标是减少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

在Pandas版本的GroupBy.apply中也可以看到这种行为<;0.25(固定为0.25,有关详细信息,请参阅此处。)

并非所有apply都是相同的

下面的图表建议何时考虑apply1。绿色意味着可能高效;红色回避。

enter image description here

一些这是直观的: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在这里通常是可以的,只要您在自定义函数中使用的方法本身是矢量化的。有时,对于您希望应用的GroupWise聚合,没有原生的Pandas方法。在这种情况下,对于少量的组_ABC,具有自定义功能的_1仍然可以提供合理的性能。

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. 与常规for循环相比,处理字符串的df.apply(pd.to_datetime)不能很好地随行缩放。

是否存在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。我想知道为什么这不是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)