我在 Stack Overflow 上看到很多关于使用 Pandas 方法 apply
的问题的答案。我还看到用户在他们下面评论说“ apply
很慢,应该避免”。
我读过很多关于性能主题的文章,解释 apply
很慢。我还在文档中看到了关于 apply
如何只是传递 UDF 的便利函数(现在似乎找不到)的免责声明。因此,普遍的共识是 apply
应该尽可能避免。然而,这提出了以下问题:
- 如果
apply
太糟糕了,那么它为什么会出现在 API 中? - 我应该如何以及何时使我的代码
apply
免费? - 是否有任何情况
apply
是 _好的_(比其他可能的解决方案更好)?
原文由 cs95 发布,翻译遵循 CC BY-SA 4.0 许可协议
apply
,你永远不需要的便捷功能我们首先逐一解决 OP 中的问题。
DataFrame.apply
和Series.apply
分别是定义在DataFrame和Series对象上的 _便捷函数_。apply
接受任何在 DataFrame 上应用转换/聚合的用户定义函数。apply
实际上是一颗灵丹妙药,可以完成任何现有熊猫函数无法完成的任务。有些事情
apply
可以做:axis=1
)或按列(axis=0
)应用函数agg
或transform
)result_type
参数)。…等等。有关详细信息,请参阅文档中的按 行或按列的函数应用。
那么,有了所有这些功能,为什么
apply
不好?这是 因为apply
很 慢。 Pandas 不会对您的函数的性质做出任何假设,因此会根据需要 迭代地将您的函数 应用于每一行/列。此外,处理上述 所有 情况意味着apply
每次迭代都会产生一些主要开销。此外,apply
消耗更多的内存,这对内存受限的应用程序来说是一个挑战。在极少数情况下
apply
适合使用(更多内容见下文)。 如果您不确定是否应该使用apply
,您可能不应该使用。让我们解决下一个问题。
换句话说,这里有一些常见的情况,您可能希望 摆脱 对
apply
的任何调用。数值数据
如果您正在处理数字数据,则可能已经有一个矢量化的 cython 函数可以完全满足您的要求(如果没有,请在 Stack Overflow 上提问或在 GitHub 上提出功能请求)。
对比
apply
的性能进行简单的加法运算。<!- ->
在性能方面,没有可比性,cythonized 等价物要快得多。不需要图表,因为即使是玩具数据,差异也很明显。
即使您允许使用
raw
参数传递原始数组,它的速度仍然是原来的两倍。另一个例子:
一般来说, 如果可能的话,寻找矢量化的替代方案。
字符串/正则表达式
Pandas 在大多数情况下都提供“矢量化”字符串函数,但在极少数情况下,这些函数不会……可以这么说。
一个常见的问题是检查某一列中的值是否存在于同一行的另一列中。
这应该返回第二行和第三行,因为“donald”和“minnie”出现在它们各自的“Title”列中。
使用应用,这将使用
但是,使用列表理解存在更好的解决方案。
<!- ->
这里要注意的是迭代例程恰好比
apply
更快,因为开销较低。如果您需要处理 NaN 和无效数据类型,您可以使用自定义函数在此基础上构建,然后您可以在列表理解中使用参数调用。有关何时应将列表推导式视为一个好的选择的更多信息,请参阅我的文章: Are for-loops in pandas really bad?我什么时候应该关心? .
一个常见的陷阱:列表的爆炸列
人们很想使用
apply(pd.Series)
。这在性能方面是 可怕 的。更好的选择是列出该列并将其传递给 pd.DataFrame。
<!- ->
最后,
Apply 是一个方便的函数,所以 有些 情况下开销可以忽略不计。这实际上取决于函数被调用了多少次。
为系列向量化的函数,但不是数据帧
如果要对多个列应用字符串操作怎么办?如果要将多列转换为日期时间怎么办?这些函数仅针对 Series 进行矢量化,因此必须将它们 应用于 您要转换/操作的每一列。
这是
apply
的可接受案例:请注意,
stack
也有意义,或者只使用显式循环。所有这些选项都比使用apply
稍微快一点,但差异小到可以原谅。您可以对其他操作(例如字符串操作或转换为类别)进行类似的处理。
比/秒
等等…
将系列转换为
str
:astype
与apply
这似乎是 API 的一个特性。使用
apply
将系列中的整数转换为字符串与使用astype
相当(有时更快)。perfplot
库绘制的。对于浮点数,我看到
astype
始终与apply
一样快,或稍快一些。所以这与测试中的数据是整数类型有关。GroupBy
链式转换操作GroupBy.apply
还没有被讨论,但是GroupBy.apply
也是一个迭代的便利函数来处理现有的GroupBy
函数不处理的任何事情。一个常见的要求是执行一个 GroupBy,然后执行两个主要操作,例如“滞后的 cumsum”:
<!- ->
您需要在此处进行两次连续的 groupby 调用:
使用
apply
,您可以将其缩短为一次调用。很难量化性能,因为它取决于数据。但总的来说,如果目标是减少
apply
是一个可接受的解决方案groupby
调用(因为groupby
也相当昂贵)。其他注意事项
除了上述注意事项之外,还值得一提的是
apply
在第一行(或第一列)上运行两次。这样做是为了确定函数是否有任何副作用。如果不是,apply
可以使用快速路径来评估结果,否则它会退回到缓慢的实现。这种行为也出现在
GroupBy.apply
在熊猫版本 <0.25 上(它已为 0.25 修复, 有关更多信息,请参见此处。)