cs9*_*s95 68 python performance apply pandas
这是一个自我回答的QnA,旨在指导用户应用的缺陷和好处.
我已经看到很多关于Stack Overflow问题的答案涉及使用apply.我也看到用户评论他们说" apply很慢",应该避免".
我已经阅读了很多关于性能主题的文章,解释apply很慢.我还在文档中看到了一个关于如何apply简单地传递UDF的便利函数的免责声明(现在似乎无法找到).因此,普遍的共识是,apply如果可能,应该避免.但是,这引发了以下问题:
apply是如此糟糕,那为什么它在API中呢?apply- 免费?apply是不错的(比其他可能的解决方案更好)?cs9*_*s95 65
apply,您永远不需要的便利功能我们首先逐一解决OP中的问题.
如果
DataFrame.apply是如此糟糕,那为什么它在API中呢?
Series.apply并且apply分别是在DataFrame和Series对象上定义的便捷函数.apply接受在DataFrame上应用转换/聚合的任何用户定义函数.apply实际上是一个银弹,无论现有的熊猫功能做什么都无法做到.
有些事情axis=1可以做:
axis=0)或column-wise(agg)函数transform或result_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也非常昂贵),这是一个可接受的解决方案.
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有例外,但这些通常是边缘的或不常见的.几个例子:
df['col'].apply(str)可能略微超越df['col'].astype(str).df.apply(pd.to_datetime)处理字符串不能很好地扩展行与常规for循环.对于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 次 |
| 最近记录: |