apply
,你永远不需要的便利功能
我们从一个接一个的回答OP中的问题开始。
如果
应用
很糟糕,那为什么在api中呢?
DataFrame.apply
和
Series.apply
是
便利功能
分别在dataframe和series对象上定义。
应用
接受对数据帧应用转换/聚合的任何用户定义函数。
应用
它实际上是一颗银弹,可以做任何现有熊猫功能所不能做的事情。
一些事情
应用
可以做到:
-
在数据帧或序列上运行任何用户定义的函数
-
按行应用函数(
axis=1
)或按列(
axis=0
)在数据帧上
-
应用函数时执行索引对齐
-
使用用户定义的函数执行聚合(但是,我们通常更喜欢
agg
或
transform
在这些情况下)
-
执行元素转换
-
将聚合结果广播到原始行(请参见
result_type
争论)。
-
接受要传递给用户定义函数的位置/关键字参数。
……以及其他。有关详细信息,请参见
Row or Column-wise Function Application
在文档中。
所以,有了这些特性,为什么
应用
不好?它是
因为
应用
是
缓慢的
. 熊猫对你的功能没有任何假设,所以
迭代应用函数
必要时发送到每行/每列。另外,处理
全部的
在上述情况下
应用
在每次迭代中都会产生一些主要的开销。此外,
应用
消耗更多的内存,这对内存有限的应用程序来说是一个挑战。
很少有情况下
应用
适合使用(更多信息见下文)。
如果你不确定是否应该使用
应用
,你可能不应该。
我们来回答下一个问题。
我应该如何以及何时编写代码
应用
-免费?
数值数据
如果您使用的是数字数据,那么很可能已经有一个矢量化的cython函数正是您想要做的(如果没有,请询问堆栈溢出问题或在github上打开一个功能请求)。
对比
应用
一个简单的加法运算。
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
这将返回第二行和第三行,因为“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)
这里要注意的是,迭代例程碰巧比
应用
,因为开销较低。如果需要处理NANS和无效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是一个方便的功能,所以
是
在这种情况下,开销是微不足道的,足以原谅。这实际上取决于函数被调用的次数。
为序列而不是数据帧矢量化的函数
如果要对多个列应用字符串操作怎么办?如果要将多个列转换为datetime怎么办?这些函数仅对序列进行矢量化,因此它们必须
应用
在要转换/操作的每个列上。
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
这是一个可以受理的案件
应用
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
请注意,这对
stack
,或者只使用显式循环。所有这些选项都比使用
应用
,但差别很小,足以让人原谅。
%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
D-型使用
astype
V/S
应用
这似乎是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()
)
有了漂浮物,我看到了
阿斯泰普
始终与
应用
. 所以这与测试中的数据是整数类型有关。
GroupBy
涉及两个功能的操作
GroupBy.apply
直到现在还没有讨论过,但是
groupby.apply组
也是一个迭代便利函数,用于处理
分组
功能没有。
一个常见的要求是先执行groupby,然后执行两个基本操作,如“lagged 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
这里需要连续两次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
使用
应用
你可以把它缩短为一个单一的呼叫。
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
打电话(因为
子句
也很贵)。