Python 过滤NumPy数组:最佳方法是什么?

Python 过滤NumPy数组:最佳方法是什么?,python,numpy,cython,numba,Python,Numpy,Cython,Numba,假设我有一个NumPy数组arr,我想对它进行元素过滤,例如。 我只想得到低于某个阈值k的值 有几种方法,例如: 使用生成器:np.fromiter((如果x

假设我有一个NumPy数组
arr
,我想对它进行元素过滤,例如。 我只想得到低于某个阈值
k
的值

有几种方法,例如:

  • 使用生成器:
    np.fromiter((如果x
  • 使用布尔掩码切片:
    arr[arr
  • 使用
    np.where()
    arr[np.where(arr
  • 使用
    np.nonzero()
    arr[np.nonzero(arr
  • 使用基于Cython的自定义实现
  • 使用基于Numba的自定义实现
  • 哪一个最快?内存效率如何


    (编辑:根据@ShadowRanger注释添加了
    np.nonzero()

    定义
  • 使用发电机:
  • 来自ITER的def过滤器(arr,k): 返回np.fromiter((如果x
  • 使用布尔掩码切片:
  • def过滤器屏蔽(arr,k):
    返回arr[arr
  • 使用
    np.where()
  • def过滤器,其中(arr,k):
    返回arr[np.where(arr
  • 使用
    np.nonzero()
  • def过滤器非零(arr,k): 返回arr[np.非零(arr
  • 使用基于Cython的自定义实现:
    • 单程
      filter\u cy()
    • 两次通过
      filter2\u cy()

  • 时间基准 基于生成器的
    filter\u fromiter()
    方法比其他方法慢得多(大约2个数量级,因此在图表中省略)

    计时将取决于输入数组大小和过滤项的百分比

    作为输入大小的函数 第一个图表说明了作为输入大小函数的计时(对于约50%过滤掉的元素):

    一般来说,基于Numba的方法始终是最快的,紧随其后的是Cython方法。其中,对于中等和较大的输入,双通道方法最快。在NumPy中,基于
    np.where()
    的方法和基于
    np.nonzero()
    的方法基本相同(除了非常小的输入,
    np.nonzero()
    似乎稍微慢一点),它们都比布尔掩码切片快,除了非常小的输入(低于~100个元素)其中布尔掩码切片速度更快。 此外,对于非常小的输入,基于Cython的解决方案比基于NumPy的解决方案慢

    作为填充的函数 第二个图表将计时作为通过过滤器的项目的函数(对于约100万个元素的固定输入大小):

    第一个观察结果是,当接近50%填充时,所有方法都是最慢的,填充更少或更多时,它们的速度更快,而不填充的速度最快(滤出值的最高百分比,通过值的最低百分比,如图的x轴所示)。 同样,Numba和Cython版本通常都比基于NumPy的版本快,Numba几乎总是最快的,Cython在图的最右边部分胜过了Numba。 值得注意的例外是,当填充接近100%时,单通道Numba/Cython版本基本上复制了大约两次,布尔掩码切片解决方案最终优于它们。 对于较大的填充阀,两次通过的方法具有增加的边际速度增益。 在NumPy中,基于
    np.where()
    的方法和基于
    np.nonzero()
    的方法基本相同。 在比较基于NumPy的解决方案时,
    np.where()
    /
    np.nonzero()
    解决方案的性能几乎总是优于布尔掩码切片,图的最右外部分除外,布尔掩码切片变得最快

    (完整代码可用)


    内存注意事项 基于生成器的
    filter\u fromiter()
    方法只需要最小的临时存储,与输入大小无关。 内存方面,这是最有效的方法。 Cython/Numba两次传递方法具有相似的内存效率,因为输出的大小是在第一次传递期间确定的

    在内存方面,Cython和Numba的单通道解决方案都需要输入大小的临时数组。 因此,这些是内存效率最低的方法

    布尔掩码切片解决方案需要输入大小的临时数组,但类型为
    bool
    ,在NumPy中为1位,因此这比典型64位系统上NumPy数组的默认大小小约64倍

    基于
    np.where()。因此,第二步根据过滤元素的数量具有可变的内存需求


    评论
    • 在指定不同的过滤条件时,生成器方法也是最灵活的
    • Cython解决方案需要指定数据类型以使其快速
    • 对于Numba和Cython,可以将过滤条件指定为通用函数(因此不需要硬编码),但必须在其各自的环境中指定过滤条件,并且必须注意确保正确编译过滤条件以提高速度,否则会出现严重的减速
    • 单程解决方案在返回之前需要额外的
      .copy()
      ,以避免浪费内存
    • NumPy方法不返回输入视图,而是返回一个副本,这是由于:
    arr=np.arange(100)
    k=50
    print('arr[arr>k]`是副本:',arr[arr>
    
    %%cython -c-O3 -c-march=native -a
    #cython: language_level=3, boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True, infer_types=True
    
    
    cimport numpy as cnp
    cimport cython as ccy
    
    import numpy as np
    import cython as cy
    
    
    cdef long NUM = 1048576
    cdef long MAX_VAL = 1048576
    cdef long K = 1048576 // 2
    
    
    cdef int smaller_than_cy(long x, long k=K):
        return x < k
    
    
    cdef size_t _filter_cy(long[:] arr, long[:] result, size_t size, long k):
        cdef size_t j = 0
        for i in range(size):
            if smaller_than_cy(arr[i]):
                result[j] = arr[i]
                j += 1
        return j
    
    
    cpdef filter_cy(arr, k):
        result = np.empty_like(arr)
        new_size = _filter_cy(arr, result, arr.size, k)
        return result[:new_size].copy()
    
    
    cdef size_t _filtered_size(long[:] arr, size_t size, long k):
        cdef size_t j = 0
        for i in range(size):
            if smaller_than_cy(arr[i]):
                j += 1
        return j
    
    
    cpdef filter2_cy(arr, k):
        cdef size_t new_size = _filtered_size(arr, arr.size, k)
        result = np.empty(new_size, dtype=arr.dtype)
        new_size = _filter_cy(arr, result, arr.size, k)
        return result