Pandas mask/where方法与NumPy np.where

jpp*_*jpp 27 python performance numpy series pandas

在有条件地更新系列中的值时,我经常使用Pandas maskwhere方法来获得更清晰的逻辑.但是,对于性能相对较高的代码,我注意到相对于性能的显着下降numpy.where.

虽然我很乐意接受特定情况,但我很想知道:

  1. 除了// 参数外, Pandas mask/ where方法是否提供任何其他功能?我理解这3个参数但很少使用它们.例如,我不知道参数引用了什么. inplaceerrorstry-castlevel
  2. 是否有任何不平凡的反例,其中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
Run Code Online (Sandbox Code Playgroud)

对于非标量值,性能似乎进一步分化:

%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
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 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)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,大部分时间花费在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
Run Code Online (Sandbox Code Playgroud)

相当不同的情况:

  • 熊猫不会PyArray_Where在引擎盖下使用- 最突出的时间消费者是vm_engine_iter_task,这是一种数字功能.
  • 有一些沉重的记忆复制正在进行 - __memmove_ssse3_back使用大约25的时间!可能一些内核的功能也连接到内存访问.

实际上,PyArray_Where在引擎盖下使用pandas-0.19 ,对于旧版本的perf报告看起来像:

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
Run Code Online (Sandbox Code Playgroud)

所以基本上它会np.where在引擎盖下使用+一些开销(所有上面的数据复制,请参阅__memmove_ssse3_back).

在Pandas的0.19版本中,我看不到大熊猫变得比numpy更快的情况 - 它只是增加了numpy功能的开销.Pandas的版本0.23.3是一个完全不同的故事 - 这里使用了numexpr-module,很可能有一些情况下pandas的版本(至少稍微)更快.

我不确定这个内存复制是否真的被称为/必要 - 也许甚至可以称之为性能错误,但我只是不知道还不确定.

我们可以通过剥离一些间接(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)
Run Code Online (Sandbox Code Playgroud)

现在,大熊猫只慢了25%.perf说:

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
Run Code Online (Sandbox Code Playgroud)

更少的数据复制,但仍然比numpy的版本更多,这是负担开销的主要原因.

我的主要内容是:

  • 大熊猫有可能至少比numpy快一点(因为它可能更快).然而,大熊猫对数据复制的处理有些不透明,这使得很难预测这种可能性何时被(不必要的)数据复制所掩盖.

  • where/ 的性能mask是瓶颈的时候,我会用numba/cython来提高性能 - 看看我在下面进一步尝试使用numba和cython.


我的想法是采取

np.where(df[0] > 0.5, df[0]*2, df[0])
Run Code Online (Sandbox Code Playgroud)

版本并消除了创建临时的需要 - 即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)
Run Code Online (Sandbox Code Playgroud)

这比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)
Run Code Online (Sandbox Code Playgroud)

加速25%.不确定,为什么cython比numba慢得多.


人数:

答: 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])  
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

  • @jpp正如我所理解的那样,如果`other`-argument有另一个维数(即更少)的维度而不是'df [0]`来控制(在...中),则使用`level`(和``s`参数一起使用)至少在某种程度上)执行对齐的方式.我很确定,它对你的表现没有任何影响,也不确定它应该是这个答案的一部分. (2认同)
  • @jpp你可能已经遵循了代码,但为了在某处/为其他人保存信息:https://github.com/pandas-dev/pandas/blob/v0.23.4/pandas/core/generic.py# L7544和https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.align.html和 (2认同)
  • 我猜Numba因为循环的SIMD矢量化而表现非常好.您可以使用llvmlite.binding将其作为llvm llvm.set_option('',' - debug-only = loop-vectorize')进行检查.使用正确的C编译器设置,Cython也可以执行相同的操作.(与Clang编译器相同的是(-O3,-march = native)) (2认同)