代码之家  ›  专栏  ›  技术社区  ›  jpp

熊猫面具/where方法与NumPy np.where

  •  27
  • jpp  · 技术社区  · 7 年前

    我经常用熊猫 mask where numpy.where .

    虽然我很乐意接受这种特殊情况,但我有兴趣知道:

    1. 熊猫吗 面具 / 方法提供任何附加功能, 除了 inplace / errors / try-cast 参数?我了解这三个参数,但很少使用它们。例如,我不知道 level 参数引用。
    2. 有没有不平凡的反例 面具 哪里 胜过 在哪里 ? 如果存在这样一个例子,它可能会影响我如何选择合适的方法。

    作为参考,这里有一些关于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
    
    1 回复  |  直到 7 年前
        1
  •  24
  •   ead    7 年前

    我使用的是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 )-我在没有根权限的情况下运行,因此无法解析符号。

    对于熊猫,我们得到(代码见附录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_在哪里 在引擎盖下-最突出的时间消费者是 vm_engine_iter_task ,这是 numexpr-functionality .
    • 正在进行一些重内存复制- __memmove_ssse3_back 使用关于 25

    实际上,熊猫-0.19使用 PyArray_在哪里

    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 在引擎盖下+一些开销(以上所有数据复制,请参见 __回忆往事 )那时候。

    我看不出熊猫在熊猫0.19版本中会比numpy快的场景——它只是增加了numpy功能的开销。熊猫的0.23.3版本是一个完全不同的故事-这里使用的是numexpr模块,很有可能在某些情况下熊猫的版本(至少稍微)更快。

    我不确定这个内存复制是否真的需要-也许有人甚至可以称之为性能缺陷,但我只是不太确定。

    我们可以通过去除一些间接的影响(通过 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
    

    我的钥匙被拿走了:

    • 熊猫有可能至少比努比快一点(因为它可能更快)。然而,pandas对数据复制的处理有些不透明,因此很难预测何时(不必要的)数据复制会掩盖这一潜力。

    • where / mask 是瓶颈,我会使用numba/cython来提高性能-请看下面我相当天真的尝试使用numba和cython。


    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)
    

    比纽比版本快5倍!

    %%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慢得多。


    列表:

    答: np_其中.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])  
    

    乙:

    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)