当前位置: 首页 > 知识库问答 >
问题:

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

司允晨
2023-03-14

我看到过许多关于堆栈溢出问题的答案,这些问题涉及使用Pandas方法apply。我也看到用户在他们下面评论说“apply速度慢,应该避免”。

  1. 如果apply是如此糟糕,那么为什么它会出现在API中?
  2. 如何和何时使我的代码应用免费?
  3. 是否有任何情况下apply是好的(优于其他可能的解决方案)?

共有1个答案

曹镜
2023-03-14

我们首先逐个解决OP中的问题。

DataFrame.applySeries.apply是分别在DataFrame和Series对象上定义的方便函数。apply接受对数据帧应用转换/聚合的任何用户定义函数。apply实际上是一个灵丹妙药,可以完成任何现有pandas函数无法完成的功能。

apply可以做的一些事情:

    null

…在其他方面。有关更多信息,请参阅文档中的按行或按列的函数应用程序

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

在很少的情况下,使用apply是合适的(下面将介绍更多内容)。如果您不确定是否应该使用apply,那么您可能不应该使用apply

让我们讨论下一个问题。

换个说法,下面是一些常见的情况,您将希望摆脱对apply的任何调用。

如果您使用的是数值数据,那么可能已经有一个矢量化的cython函数可以实现您想要实现的功能(如果没有,请询问堆栈溢出问题或在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)

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

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

使用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)
s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object
s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

更好的选择是listify列并将其传递给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)

最后,

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)

等等...

这似乎是API的一个特点。使用apply将序列中的整数转换为字符串与使用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())

对于浮点,我看到aStype始终与apply一样快,或者略快于apply。所以这与测试中的数据是整数类型有关。

groupby.apply直到现在才被讨论过,但是groupby.apply也是一个迭代方便函数,可以处理现有groupby函数没有的任何内容。

一个常见的要求是执行GroupBy和两个prime操作,例如“滞后的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

<!-->

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
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对第一行(或列)操作两次。这样做是为了确定函数是否有任何副作用。如果不是,则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版本中被修复,更多信息请参阅此处)

 类似资料:
  • 问题内容: 在工作中进行大量重构的中间,我希望引入stdClass *作为从函数返回数据的一种方式,并且我试图找到非主观论据来支持我的决定。 是否有任何情况下最好使用一种而不是另一种? 使用stdClass而不是数组有什么好处? 有人会说,函数必须尽可能少且特定,才能返回一个值。 我决定使用stdClass是暂时的,因为从长远来看,我希望为每个进程找到正确的Value Objects。 问题答案:

  • 问题内容: 有什么区别?什么时候应该使用容量为1的对抗? 问题答案: SynchronousQueue更像是一个传递,而LinkedBlockingQueue仅允许单个元素。区别在于对SynchronousQueue的put()调用直到有相应的take()调用 才返回 ,但LinkedBlockingQueue的大小为1,则put()调用(对空队列)将立即返回。 我不能说自己曾经直接使用过Sync

  • 问题内容: 我对使用和翻译有疑问。我了解到,在模型中,我应该使用。但是还有其他地方我也应该使用吗?表单定义呢?它们之间是否存在性能差异? 编辑: 还有一件事。有时候,代替被使用。正如文档所述,仅在将字符串显示给用户之前,才将字符串标记为要翻译,并在可能的最新情况下进行翻译,但是我在这里有点困惑,这与功能相似吗?我仍然很难决定在模型和表格中应该使用哪个。 问题答案: ugettext() 与 uge

  • 我想知道什么时候可以有效地使用。我不确定到底有多有用,有三个原因。 (请将start和end视为整数。) > 如果我想要一个数组,,下面的代码要快得多。 我不认为仅仅获取从到的数字是有用的。我可以将

  • 问题内容: 我项目的一位主要开发人员已将项目的toString()实现称为“纯粹的残障”,并希望将其从代码库中删除。 我已经说过,这样做意味着任何希望显示对象的客户端都必须编写自己的代码以将对象转换为字符串,但这得到了“是的答案”。 现在具体来说,该系统中的对象是矩形,圆形等图形元素,当前表示形式是显示x,y,比例,边界等。 那么,人群在哪里呢? 您应该何时以及何时不应该实现toString? 问

  • 问题内容: 我知道他们两个都禁用了Nagle的算法。 我什么时候应该/不应该使用它们中的每一个? 问题答案: 首先,不是所有人都禁用Nagle的算法。 Nagle的算法用于减少有线中更多的小型网络数据包。该算法是:如果数据小于限制(通常是MSS),请等待直到收到先前发送的数据包的ACK,同时累积用户的数据。然后发送累积的数据。 这将对telnet等应用程序有所帮​​助。但是,在发送流数据时,等待A