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

cs9*_*s95 68 python performance apply pandas

这是一个自我回答的QnA,旨在指导用户应用的缺陷和好处.

我已经看到很多关于Stack Overflow问题的答案涉及使用apply.我也看到用户评论他们说" apply很慢",应该避免".

我已经阅读了很多关于性能主题的文章,解释apply很慢.我还在文档中看到了一个关于如何apply简单地传递UDF的便利函数的免责声明(现在似乎无法找到).因此,普遍的共识是,apply如果可能,应该避免.但是,这引发了以下问题:

  1. 如果apply是如此糟糕,那为什么它在API中呢?
  2. 我应该如何以及何时制作我的代码apply- 免费?
  3. 是否有过任何地方的情况apply不错的(比其他可能的解决方案更好)?

cs9*_*s95 65

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

我们首先逐一解决OP中的问题.

如果DataFrame.apply是如此糟糕,那为什么它在API中呢?

Series.apply并且apply分别是在DataFrame和Series对象上定义的便捷函数.apply接受在DataFrame上应用转换/聚合的任何用户定义函数.apply实际上是一个银弹,无论现有的熊猫功能做什么都无法做到.

有些事情axis=1可以做:

  • 在DataFrame或Series上运行任何用户定义的函数
  • 在DataFrame上应用row-wise(axis=0)或column-wise(agg)函数
  • 应用函数时执行索引对齐
  • 使用用户定义的函数执行聚合(但是,我们通常更喜欢transformresult_type在这些情况下)
  • 执行逐元素转换
  • 将聚合结果广播到原始行(请参阅apply参数).
  • 接受位置/关键字参数以传递给用户定义的函数.

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

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

很少apply有适合使用的情况(更多内容见下文).如果你不确定是否应该使用apply,你可能不应该.


让我们来解决下一个问题.

我应该如何以及何时制作我的代码apply- 免费?

数字数据
如果您正在处理数字数据,可能已经有一个矢量化的cython函数,它正是您正在尝试做的事情(如果没有,请在Stack Overflow上提问或在GitHub上打开一个功能请求).

对比raw简单的加法操作的性能.

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
Run Code Online (Sandbox Code Playgroud)

df.apply(np.sum)

A    16
B    28
dtype: int64

df.sum()

A    16
B    28
dtype: int64
Run Code Online (Sandbox Code Playgroud)

性能方面,没有比较,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)
Run Code Online (Sandbox Code Playgroud)

即使您使用apply参数启用传递原始数组,它仍然是两倍慢.

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

另一个例子:

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)
Run Code Online (Sandbox Code Playgroud)

一般来说,如果可能的话,寻找矢量化的替代品.


字符串/正则表达式
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
Run Code Online (Sandbox Code Playgroud)

这应该返回第二行和第三行,因为"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
Run Code Online (Sandbox Code Playgroud)

但是,使用列表推导存在更好的解决方案.

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
Run Code Online (Sandbox Code Playgroud)

%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)
Run Code Online (Sandbox Code Playgroud)

这里需要注意的是,迭代例程碰巧要快pd.to_datetime(df['date']),因为开销较低.如果你需要处理NaN和无效的dtypes,你可以使用自定义函数在此基础上构建,然后可以使用list comprehension中的参数调用它.

有关何时应将列表推导视为一个好选项的更多信息,请参阅我的文章:对于带有pandas的循环 - 我应该何时关注?.

注意
日期和日期时间操作也有矢量化版本.所以,例如,你应该更喜欢df['date'].apply(pd.to_datetime),比如说apply(pd.Series).

阅读更多 文档.


爆炸列的列

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

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object
Run Code Online (Sandbox Code Playgroud)

人们很想使用apply.这在性能方面很糟糕.

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2
Run Code Online (Sandbox Code Playgroud)

更好的选择是对列进行分类并将其传递给pd.DataFrame.

pd.DataFrame(s.tolist())

   0  1
0  1  2
1  1  2
2  1  2
Run Code Online (Sandbox Code Playgroud)

%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)
Run Code Online (Sandbox Code Playgroud)

最后,

是否有过任何地方的情况apply好的

应用是一个方便的功能,所以这里的开销可以忽略不计足以原谅的情况.这实际上取决于调用函数的次数.

为Series而不是DataFrames Vectorized的函数
如果要对多列应用字符串操作,该怎么办?如果要将多列转换为日期时间怎么办?这些函数仅针对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
Run Code Online (Sandbox Code Playgroud)

这是一个可以接受的案例stack:

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

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object
Run Code Online (Sandbox Code Playgroud)

请注意,它也有意义apply,或者只使用显式循环.所有这些选项都比使用稍快str,但差异小到可以原谅.

%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)
Run Code Online (Sandbox Code Playgroud)

您可以为其他操作(例如字符串操作或转换为类别)创建类似的案例.

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

等等...


astype使用applyv/s 将Series转换为dtypeapply
这似乎是API的特性.使用astype到整数转换的一系列字符串比使用可比的(有时甚至更快)perfplot.

在此输入图像描述 使用该astype库绘制图表.

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())
Run Code Online (Sandbox Code Playgroud)

对于花车,我看到apply它始终如此快或稍快GroupBy.所以这与测试中的数据是整数类型的事实有关.


GroupBy.apply
GroupBy.apply直到现在还没有讨论涉及两个函数的操作,但GroupBy它也是一个迭代的便利函数来处理现有apply函数所没有的任何东西.

一个常见的要求是执行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
Run Code Online (Sandbox Code Playgroud)

你需要在这里进行两次连续的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
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

很难量化性能,因为它取决于数据.但总的来说,groupby如果目标是减少groupby呼叫(因为apply也非常昂贵),这是一个可接受的解决方案.

  • 我认为“有没有什么情况下 apply 是好的?”的另一个答案。这个答案就说明了这一点。请注意,一般来说,与不考虑它并使用“apply”相比,不使用“apply”的解决方案要复杂得多,因此更容易出错。因此,就像在软件开发以及一般生活中一样,您可能希望应用 80-20 规则。80% 的时间使用 apply 是首选。但在 20% 的情况下,结果太慢,您可以继续优化,远离“apply”。 (4认同)

jpp*_*jpp 28

所有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.applycolumn-wise(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
Run Code Online (Sandbox Code Playgroud)

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

  1. df['col'].apply(str)可能略微超越df['col'].astype(str).
  2. df.apply(pd.to_datetime)处理字符串不能很好地扩展行与常规for循环.


Pet*_*ppi 7

对于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)
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

8380 次

最近记录:

6 年,2 月 前