这是一个自我解答的QnA,旨在指导用户有关申请的陷阱和好处。
我已经看到很多有关应用程序使用堆栈溢出问题的答案。我还看到用户在他们的下面发表评论,说“ testArr
很慢”,应避免使用。”
我已经阅读了许多有关性能主题的文章,这些文章解释了apply
很慢。我还在文档中看到了关于apply
仅仅是传递UDF的便捷函数的免责声明(现在似乎找不到)。因此,通常的共识是,应尽可能避免使用apply
。但是,这引发了以下问题:
apply
太糟糕了,那为什么在API中会这样呢?apply
不受限制?apply
是好(比其他可能的解决方案更好)的情况吗? 答案 0 :(得分:39)
apply
,您不需要的便捷功能我们首先在OP中逐一解决问题。
如果
apply
太糟糕了,那为什么在API中会这样呢?
DataFrame.apply
和Series.apply
是分别在DataFrame和Series对象上定义的便捷功能。 apply
接受任何在DataFrame上应用转换/聚合的用户定义函数。 apply
实际上是万事大吉,可以完成任何现有的熊猫功能无法完成的工作。
apply
可以做的一些事情:
axis=1
)或按列(axis=0
)应用函数agg
或transform
)result_type
参数)。...等等。有关更多信息,请参阅文档中的Row or Column-wise Function Application。
因此,使用所有这些功能,apply
为什么不好? 是因为apply
慢。 Pandas不对您的函数的性质做任何假设,因此迭代地将您的函数应用于每个行/列。另外,处理上述所有情况都意味着apply
在每次迭代中都会产生一些重大开销。此外,apply
会消耗更多的内存,这对于内存受限的应用程序是一个挑战。
在极少数情况下,apply
是适合使用的(下面有更多介绍)。 如果不确定是否应使用apply
,则可能不应该使用。
让我们解决下一个问题。
我应如何以及何时使代码
apply
不受限制?
数值数据
如果您正在使用数字数据,则可能已经有一个矢量化的cython函数可以完全实现您要执行的操作(否则,请在Stack Overflow上提问或在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
这应该返回第二行和第三行,因为“唐纳德”和“米妮”分别出现在其“标题”列中。
使用套用,可以使用
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和无效的dtype,则可以使用自定义函数在此基础上进行构建,然后再使用列表推导中的参数进行调用。
有关何时应将列表理解视为一个不错的选择的更多信息,请参阅我的文章:For loops with pandas - When should I care?。
注意
日期和日期时间操作也具有矢量化版本。因此,例如,您应该更喜欢pd.to_datetime(df['date'])
, 例如df['date'].apply(pd.to_datetime)
。在 docs。
列表的爆炸列
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是一项便利功能,因此在 情况下,开销可以忽略不计,可以原谅。这实际上取决于调用该函数的次数。
针对系列进行矢量化的功能,但不针对数据框
如果要对多列应用字符串操作怎么办?如果要将多列转换为日期时间怎么办?这些函数仅对系列进行矢量化处理,因此必须在要转换/对其进行操作的每一列上应用。
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))
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)
依此类推...
使用str
v / s astype
将系列转换为apply
dtype
这似乎是API的特质。与使用apply
相比,使用astype
将Series中的整数转换为字符串是可比的(有时更快)。
该图是使用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
一样快,或稍快。因此,这与测试中的数据是整数类型有关。
GroupBy
操作涉及两个功能
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
量化性能非常困难,因为它取决于数据。但是总的来说,如果目标是减少apply
的通话,则groupby
是可以接受的解决方案(因为groupby
也很昂贵)。
答案 1 :(得分:20)
apply
都不一样下面的图表建议何时考虑使用apply
1 。绿色意味着高效。红色避免。
有些很直观: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.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 有例外,但通常很少或很少。几个例子:
df['col'].apply(str)
的表现可能略好于df['col'].astype(str)
。df.apply(pd.to_datetime)
循环相比,for
处理字符串在行上的缩放效果不佳。答案 2 :(得分:1)
我想加两分钱:
在任何情况下,申请都是好的吗? 是的,有时候。
任务:解码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的提醒,我忘记了普通的名单理解。
答案 3 :(得分:1)
对于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)