我已经看到许多有关使用Pandas
方法的堆栈溢出问题的答案apply
。我还看到用户在他们的评论下说“ apply很慢,应该避免”。
我已经阅读了许多有关性能的文章,这些文章解释apply
得很慢。我还在文档中看到了关于免除apply传递UDF的便捷功能的免责声明(现在似乎找不到)。因此,普遍的共识是,apply
应尽可能避免。但是,这引起了以下问题:
apply
免费?apply
都存在良好的情况(优于其他可能的解决方案)吗?apply,你根本不需要的便利功能
我们首先在OP中逐一解决问题。
“ 如果应用是如此糟糕,那么为什么要在API中使用它呢? ”
DataFrame.apply
和Series.apply
是分别在DataFrame
和Series
对象上定义的便捷函数。apply接受任何在DataFrame
上应用转换/聚合的用户定义函数。apply实际上是完成任何现有熊猫功能无法完成的灵丹妙药。
一些事情apply可以做到:
…其他 有关更多信息,请参见文档中的行或列函数应用程序。
那么,具有所有这些功能,为什么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
这应该返回第二行和第三行,因为“唐纳德”和“米妮”分别出现在它们的“标题”列中。
使用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和无效的dtypes,则可以使用自定义函数在此基础上进行构建,然后可以在列表推导中使用参数进行调用。
有关何时应该将列表理解视为一个不错的选择的更多信息,请参阅我的文章:For循环使用熊猫-我何时应该关心?。
注意
日期和日期时间操作也具有矢量化版本。因此,例如,你应该更喜欢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是一项便利功能,因此在某些情况下开销可以忽略不计,可以原谅。它实际上取决于函数被调用多少次。
为系列矢量化的函数,但不是DataFrames的函数
如果要对多列应用字符串操作该怎么办?如果要将多列转换为日期时间怎么办?这些函数仅对系列进行矢量化处理,因此必须将它们应用于要转换/操作的每一列。
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)
等等…
将Series转换为str:astypevsapply
这似乎是API的特质。与使用相比,apply用于将Series中的整数转换为字符串可比(有时更快)astype。
在此处输入图片说明 使用该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它也很昂贵)。
其他注意事项
除了上述注意事项外,还值得一提的是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
在GroupBy.apply
小于0.25
的熊猫版本中也可以看到此行为(已固定为0.25,有关更多信息,请参见此处。)
我看到过许多关于堆栈溢出问题的答案,这些问题涉及使用Pandas方法。我也看到用户在他们下面评论说“速度慢,应该避免”。 如果是如此糟糕,那么为什么它会出现在API中? 如何和何时使我的代码免费? 是否有任何情况下是好的(优于其他可能的解决方案)?
问题内容: 在工作中进行大量重构的中间,我希望引入stdClass *作为从函数返回数据的一种方式,并且我试图找到非主观论据来支持我的决定。 是否有任何情况下最好使用一种而不是另一种? 使用stdClass而不是数组有什么好处? 有人会说,函数必须尽可能少且特定,才能返回一个值。 我决定使用stdClass是暂时的,因为从长远来看,我希望为每个进程找到正确的Value Objects。 问题答案:
问题内容: 在某些情况下,我喜欢将缓冲显示为的功能,这会给我带来好处,但程序包名称使我有点担心在生产代码中使用它。我了解它 主要 用于测试,但是不确定我是否在生产代码中使用它时遇到麻烦。 我在吸引命运吗? 问题答案: 您不应在生产中使用此代码。无论您要实现什么目标。 最终目的是促进 测试 ,因此始终会在考虑测试的情况下进行扩展,优化( 重写) 。只要可以使该软件包更好地进行测试,就可以在该软件包中
问题内容: 我不太清楚当我这样说时会做什么: 我可以稍微了解使用它的原因,因为它不浏览整个数据库表以更新字段,而是仅更新新添加的记录。如我错了请纠正我。 所以我想做的是使用这样创建索引: 一旦执行此操作,文档就根本不会上传到索引。我要去哪里错了? 任何帮助,不胜感激。 问题答案: 如果您的表中有一个时间戳列(例如),则最好使用它代替ID号。这样,当记录更新时,您也可以修改该时间戳,输入插件将提取记
问题内容: 在Objective- C中,我调用该方法来记录异常。如何在Swift中调用它? 问题答案: 使用Swift 2,您可以将Swift函数和闭包作为C函数指针传递 原始答案 从Xcode 6 beta 6开始,您不能这样做。 Swift确实支持传递函数指针,但是它们几乎就像不透明指针一样被对待。您既不能定义指向Swift函数的C函数指针,也不能在Swift中调用C函数指针。 这意味着您从
问题内容: 这个问题在很多地方都被问到,变化不尽。例如Java-getClassLoader()。getResource()不仅使我烦恼不已。)我仍然无法使其正常工作。 这是一个代码片段: 此作品-请注意,我硬编码的路径,包含剪辑文件,该文件的目录 是 有,而且 是 在同一目录作为我的.class文件。las,注释掉的代码仅返回url的空值。 其他大多数帖子似乎都涉及getResourceAsSt