在有条件地更新一系列值时,我经常使用熊猫mask
和where
方法来获得更清晰的逻辑。但是,对于性能相对关键的代码,我注意到相对于numpy.where
,性能明显下降。
虽然我很高兴在特定情况下接受此要求,但我很想知道:
mask
/ where
/ inplace
参数以外,Pandas errors
/ try-cast
方法是否提供其他功能吗?我了解这三个参数,但很少使用它们。例如,我不知道level
参数指的是什么。mask
/ where
胜过numpy.where
?如果存在这样的示例,则可能会影响我以后如何选择适当的方法。作为参考,以下是Pandas 0.19.2 / Python 3.6.0的一些基准测试:
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
assert (df[0].mask(df[0] > 0.5, 1).values == np.where(df[0] > 0.5, 1, df[0])).all()
%timeit df[0].mask(df[0] > 0.5, 1) # 145 ms per loop
%timeit np.where(df[0] > 0.5, 1, df[0]) # 113 ms per loop
对于非标量值,性能似乎进一步 :
%timeit df[0].mask(df[0] > 0.5, df[0]*2) # 338 ms per loop
%timeit np.where(df[0] > 0.5, df[0]*2, df[0]) # 153 ms per loop
答案 0 :(得分:22)
我正在使用pandas 0.23.3和Python 3.6,因此仅在您的第二个示例中,我才能看到运行时间的真正差异。
但是让我们研究一下第二个示例的一个稍有不同的版本(这样我们就可以避免使用2*df[0]
)。这是我们在计算机上的基准:
twice = df[0]*2
mask = df[0] > 0.5
%timeit np.where(mask, twice, df[0])
# 61.4 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df[0].mask(mask, twice)
# 143 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpy的版本比熊猫快2.3倍。
因此,让我们对这两个函数进行概要分析以了解两者之间的区别-当人们对代码基础不太熟悉时,进行概要分析是一种了解全局的好方法:它比调试更快,并且比尝试找出错误更容易出错仅仅通过阅读代码就可以了。
我在Linux上,使用perf
。对于numpy的版本,我们得到(有关列表,请参阅附录A):
>>> perf record python np_where.py
>>> perf report
Overhead Command Shared Object Symbol
68,50% python multiarray.cpython-36m-x86_64-linux-gnu.so [.] PyArray_Where
8,96% python [unknown] [k] 0xffffffff8140290c
1,57% python mtrand.cpython-36m-x86_64-linux-gnu.so [.] rk_random
我们可以看到,大部分时间都花在PyArray_Where
中-大约占69%。未知符号是一个内核函数(实际上是clear_page
)-我在没有root特权的情况下运行,因此该符号无法解析。
对于大熊猫,我们可以获得(代码请参见附录B):
>>> perf record python pd_mask.py
>>> perf report
Overhead Command Shared Object Symbol
37,12% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
23,36% python libc-2.23.so [.] __memmove_ssse3_back
19,78% python [unknown] [k] 0xffffffff8140290c
3,32% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
1,48% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
另一种情况:
PyArray_Where
-最耗时的时间是vm_engine_iter_task
,即numexpr-functionality。__memmove_ssse3_back
大约使用25
%的时间!内核的某些功能可能也与内存访问有关。实际上,pandas-0.19在引擎盖下使用了PyArray_Where
,对于较旧的版本,perf-report的报告将如下所示:
Overhead Command Shared Object Symbol
32,42% python multiarray.so [.] PyArray_Where
30,25% python libc-2.23.so [.] __memmove_ssse3_back
21,31% python [kernel.kallsyms] [k] clear_page
1,72% python [kernel.kallsyms] [k] __schedule
因此,基本上,那时它会在后台使用np.where
+一些开销(所有数据复制,请参见__memmove_ssse3_back
)。
在熊猫的0.19版中,我看不到大熊猫会比numpy更快的情况-它只是增加了numpy功能的开销。熊猫的0.23.3版本是一个完全不同的故事-这里使用了numexpr-module,在某些情况下,熊猫的版本可能(至少稍微快一些)。
我不确定是否真的需要/必须进行这种内存复制-也许有人甚至可以称它为性能错误,但我只是不知道可以肯定什么。
我们可以通过剥离一些间接信息(传递np.array
而不是pd.Series
来帮助熊猫不要复制。例如:
%timeit df[0].mask(mask.values > 0.5, twice.values)
# 75.7 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
现在,熊猫只慢25%。性能说明:
Overhead Command Shared Object Symbol
50,81% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
14,12% python [unknown] [k] 0xffffffff8140290c
9,93% python libc-2.23.so [.] __memmove_ssse3_back
4,61% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
2,01% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
数据复制要少得多,但比numpy的版本要多,后者主要负责开销。
我的主要收获
pandas的潜力至少比numpy稍快(因为可能更快)。但是,熊猫对数据复制的处理有些不透明,因此很难预测何时(由于不必要的数据复制)这种潜力被掩盖了。
当where
/ mask
的性能成为瓶颈时,我将使用numba / cython来提高性能-请参阅下文中我比较幼稚的尝试使用numba和cython。< / p>
这个想法是要
np.where(df[0] > 0.5, df[0]*2, df[0])
版本并消除创建临时文件的需要-即df[0]*2
。
由@ max9111使用numba提议:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert(np.where(df[0] > 0.5, twice, df[0])==nb_where(df[0].values)).all()
%timeit np.where(df[0] > 0.5, df[0]*2, df[0])
# 85.1 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit nb_where(df[0].values)
# 17.4 ms ± 673 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
大约比numpy的版本快5倍!
这是我在Cython的帮助下提高性能的尝试,远未成功:
%%cython -a
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[np.float64_t] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert (df[0].mask(df[0] > 0.5, 2*df[0]).values == cy_where(df[0].values)).all()
%timeit cy_where(df[0].values)
# 66.7± 753 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
使速度提高25%。不确定,为什么cython比numba慢得多。
列表:
A: np_where.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
for _ in range(50):
np.where(df[0] > 0.5, twice, df[0])
B: pd_mask.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
mask = df[0] > 0.5
for _ in range(50):
df[0].mask(mask, twice)