我使用的是 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])
%timeit df[0].mask(mask, twice)
Numpy的版本大约比pandas快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 权限运行,因此该符号未被解析。
而对于 pandas,我们得到以下结果(有关代码请参见附录 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
情况大不相同:
- pandas在底层不使用
PyArray_Where
——最耗时的是vm_engine_iter_task
,它是numexpr功能。
- 有一些严重的内存复制正在进行——
__memmove_ssse3_back
约占25
%的时间!也许一些内核函数也与内存访问有关。
实际上,pandas-0.19在底层使用了PyArray_Where
,对于旧版本来说,性能报告会看起来像这样:
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
)。
我想在 pandas 版本 0.19 中没有场景能让 pandas 变得比 numpy 更快 - 它只是为 numpy 的功能增加了开销。然而,在完全不同的故事中,pandas 版本 0.23.3 使用了 numexpr 模块,很可能存在某些场景使得 pandas 的版本(至少略微)更快。
我不确定这种内存复制是否确实需要/必要 - 或许它甚至被称为性能缺陷,但我不确定知道得足够多。我们可以通过剥离一些间接层(传递 np.array
而非 pd.Series
)来帮助 pandas 不进行复制。例如:
%timeit df[0].mask(mask.values > 0.5, twice.values)
现在,pandas 只慢了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版本少了很多,但仍然比numpy版本多,这主要是导致过度开销的原因。
我的主要收获:
思路是取
np.where(df[0] > 0.5, df[0]*2, df[0])
使用 Numba 提出的方法,可以提高版本,并消除创建临时变量的需要 - 例如,df[0]*2
。
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])
%timeit nb_where(df[0].values)
这个方法比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)
提升了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)
96.9ms vs 92.7ms
,而对于第二个例子,我得到了276ms vs 120ms
。 - eadnumba
只能处理数字输入,对于我的用例来说这很好。如果有一个临时掩码数组,我想mask
/where
永远无法超过np.where
。 - jpp