apply
, 你从未需要过的便利函数
我们逐个回答OP中的问题。
"如果 apply
这么差,为什么还在API中呢?"
DataFrame.apply
和Series.apply
是分别定义在DataFrame和Series对象上的便利函数。 apply
接受任何用户定义的函数,对DataFrame应用变换/聚合操作。 apply
实际上是一颗银弹,可以做任何现有的pandas函数无法完成的事情。
apply
可以完成以下一些工作:
- 在DataFrame或Series上运行任何用户定义的函数
- 在DataFrame上逐行(
axis=1
)或逐列(axis=0
)地应用函数
- 应用函数时进行索引对齐
- 使用用户定义的函数进行聚合(但是,在这些情况下,我们通常更喜欢使用
agg
或transform
)
- 执行逐元素的转换
- 将聚合结果广播到原始行(参见
result_type
参数)。
- 接受位置/关键字参数以传递给用户定义的函数。
...等等。有关更多信息,请参见文档中的逐行或逐列应用函数。
那么,拥有所有这些功能,为什么apply
不好? 这是因为apply
很慢。 Pandas对您的函数的性质不作任何假设,因此必要时iteratively applies your function
每行/每列。 此外,处理上述所有情况意味着在每次迭代中都会产生一些重大的开销。进一步地,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
从性能上来看,没有可比性,经过Cython优化后的等效代码要快得多。不需要绘制图表,因为即使对于玩具数据,差异也很明显。
%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”和“Minnie”分别出现在其相应的“Title”列中,因此这应返回第二行和第三行。
使用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和无效数据类型,可以使用自定义函数来构建并在列表推导式内部调用。
有关何时应将列表推导式视为良好选项的更多信息,请参见我的写作:Are for-loops in pandas really bad? When should I care?
请注意:
日期和日期时间操作也具有向量化版本。因此,例如,您应该优先使用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)
最后,
"是否有适用于apply
的情况?"
apply
是一个方便的函数,因此在开销可以忽略不计时,确实存在可以使用它的情况。这取决于函数被调用的次数。
仅对Series向量化但不对DataFrames向量化的函数
如果您想要对多个列应用字符串操作怎么办?如果您想要将多个列转换为日期时间格式怎么办?这些函数仅对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
这是适用于 apply
的一个可接受的情况:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
请注意,
stack
或者使用显式循环也是有意义的。所有这些选项都比使用
apply
稍微快一点,但差异足够小,可以原谅。
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)
将Series转换为str
: astype
与apply
的区别
使用apply
将Series中的整数转换为字符串比使用astype
更快,这似乎是API的奇特之处。
该图是使用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
由于性能取决于数据,很难量化性能。但是通常情况下,如果目标是减少 groupby
调用(因为 groupby
也很昂贵),那么 apply
是一个可接受的解决方案。
其他注意事项
除了上面提到的注意事项之外,值得一提的是,apply
在第一行(或列)上操作两次,以确定该函数是否具有任何副作用。如果没有,apply
可能能够使用快速路径来评估结果;否则,它会回退到慢速实现。
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
A B
0 1 x
1 2 y
这种行为在pandas版本<0.25的GroupBy.apply
中也有出现(在0.25版本中已修复,更多信息请参见此处)。
returns.add(1).apply(np.log)
和np.log(returns.add(1)
是一种情况,其中apply
通常会稍微快一些,这是 jpp 图表中右下角的绿色框。 - Alexanderapply
方法通常速度很快,是一个非常好的API,有80%的时间可以使用。因此,我完全不同意那些建议不要使用它的观点。但是,了解其局限性并掌握一些技巧是明智的,以防万一apply
方法变得太慢,可以在顶部答案中列出的技巧中找到解决方案。 - Zephaniah Grunschlag