基于第三个列表过滤列表列表的最快方法?

时间:2017-05-10 14:36:47

标签: python performance numpy

我有一个列表A,如下所示:

A = np.array([[1,2] ,
              [2,4] ,
              [3,4] , 
              [4,5] , 
              [6,7]]) 

我需要删除包含第三个列表B中任何元素的所有子列表。

所以,例如:

B = [1,2,5]

预期结果将是:

np.array([[3,4] ,
          [6,7]]) 

A的长度达到1,500,000,B也经常出现在成千上万的元素中,因此性能至关重要。 A的子列表长度始终为2.

2 个答案:

答案 0 :(得分:4)

此处介绍的所有方法均基于numpys boolean indexing。方法是识别匹配(独立于行),然后沿着行使用缩减(np.anynp.all)来查看应该删除哪些行以及应该保留哪些行。最后,此掩码将应用于数组A以仅获取有效行。这些方法之间唯一真正的区别在于如何创建蒙版。

方法1:

如果事先知道B的值,则通常使用|(或运算符)链式比较。

a[~np.any(((a == 1) | (a == 2) | (a == 5)), axis=1)]

我会逐步完成这个步骤:

  1. 寻找匹配

    >>> ((a == 1) | (a == 2) | (a == 5))
    array([[ True,  True],
           [ True, False],
           [False, False],
           [False,  True],
           [False, False]], dtype=bool)
    
  2. 检查每一行True

    >>> np.any(((a == 1) | (a == 2) | (a == 5)), axis=1)
    array([ True,  True, False,  True, False], dtype=bool)
    
  3. 反转它:

    >>> ~np.any(((a == 1) | (a == 2) | (a == 5)), axis=1)
    array([False, False,  True, False,  True], dtype=bool)
    
  4. 使用布尔索引:

    >>> a[~np.any(((a == 1) | (a == 2) | (a == 5)), axis=1)]
    array([[3, 4],
           [6, 7]])
    
  5. 方法2:

    您可以使用np.in1d

    代替这些a == 1 | a == 2 | ...
    >>> np.in1d(a, [1, 2, 5]).reshape(a.shape)
    array([[ True,  True],
           [ True, False],
           [False, False],
           [False,  True],
           [False, False]], dtype=bool)
    

    然后使用与上面基本相同的方法

    >>> a[~np.any(np.in1d(a, [1, 2, 5]).reshape(a.shape), axis=1)]
    array([[3, 4],
           [6, 7]])
    

    方法3:

    如果b已排序,您还可以使用np.searchsorted创建遮罩:

    >>> np.searchsorted([1, 2, 5], a, side='left') == np.searchsorted([1, 2, 5], a, side='right')
    array([[False, False],
           [False,  True],
           [ True,  True],
           [ True, False],
           [ True,  True]], dtype=bool)
    

    这一次,您需要检查覆盖行中的all值是否为True

    >>> b = [1, 2, 5]
    >>> a[np.all(np.searchsorted(b, a, side='left') == np.searchsorted(b, a, side='right'), axis=1)]
    array([[3, 4],
           [6, 7]])
    

    时序:

    第一种方法并不完全适用于仲裁B,因此我不会将其包含在这些时间中。

    import numpy as np
    
    def setapproach(A, B):  # author: Max Chrétien
        B = set(B)
        indices_to_del = [i for i, sublist in enumerate(A) if B & set(sublist)]
        C = np.delete(A, indices_to_del, 0)
        return C
    
    def setapproach2(A, B):  # author: Max Chrétien & Ev. Kounis
        B = set(B)
        return np.array([sublist for sublist in A if not B & set(sublist)])
    
    def isinapproach(a, b):
        return a[~np.any(np.in1d(a, b).reshape(a.shape), axis=1)]
    
    def searchsortedapproach(a, b):
        b.sort()
        return a[np.all(np.searchsorted(b, a, side='left') == np.searchsorted(b, a, side='right'), axis=1)]
    
    A = np.random.randint(0, 10000, (100000, 2))
    B = np.random.randint(0, 10000, 2000)
    
    %timeit setapproach(A, B)
    # 929 ms ± 16.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit setapproach2(A, B)
    # 1.04 s ± 13.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit isinapproach(A, B)
    # 59.1 ms ± 1.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    %timeit searchsortedapproach(A, B)
    # 56.1 ms ± 1.05 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    如果B已经排序且长度为AB,则时间取决于值的范围。但是numpy接近接缝的速度几乎是设定解决方案的20倍。然而,差异主要是因为使用python循环对numpy-arrays进行迭代的效率非常低,所以我首先将AB转换为list

    def setapproach_updated(A, B):
        B = set(B)
        indices_to_del = [i for i, sublist in enumerate(A.tolist()) if B & set(sublist)]
        C = np.delete(A, indices_to_del, 0)
        return C
    
    def setapproach2_updated(A, B):
        B = set(B)
        return np.array([sublist for sublist in A.tolist() if not B & set(sublist)])
    

    这可能看起来很奇怪,但让我们重做时间:

    %timeit setapproach_updated(A, B)
    # 300 ms ± 2.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit setapproach2_updated(A, B)
    # 378 ms ± 10.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    这比普通循环要快得多,只需先将其转换为tolist,但仍然比numpy方法慢5倍。

    请记住:当您必须在NumPy阵列上使用基于Python的方法时,检查是否可以更快地将其转换为列表!

    让我们看看它在更大的数组上的表现(这些大小与你问题中提到的大小相近):

    A = np.random.randint(0, 10000000, (1500000, 2))
    B = np.random.randint(0, 10000000, 50000)
    
    %timeit setapproach_updated(A, B)
    # 4.14 s ± 66.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit setapproach2_updated(A, B)
    # 6.33 s ± 95.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit isinapproach(A, B)
    # 2.39 s ± 102 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    %timeit searchsortedapproach(A, B)
    # 1.34 s ± 21.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    差异越小,searchsorted - 方法肯定会获胜。

    方法4:

    我还没完呢!让我用给你一个惊喜,它不是一个轻量级的包但是极其强大的如果它支持你需要的类型和功能:

    import numba as nb
    
    @nb.njit                # the magic is this decorator
    def numba_approach(A, B):
        Bset = set(B)
        mask = np.ones(A.shape[0], dtype=nb.bool_)
        for idx in range(A.shape[0]):
            for item in A[idx]:
                if item in Bset:
                    mask[idx] = False
                    break
        return A[mask]
    

    让我们看看它的表现如何:

    A = np.random.randint(0, 10000, (100000, 2))
    B = np.random.randint(0, 10000, 2000)
    
    numba_approach(A, B)   # numba needs a warmup run because it's just-in-time compiling
    
    %timeit numba_approach(A, B)
    # 6.12 ms ± 145 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
    # This is 10 times faster than the fastest other approach!
    
    A = np.random.randint(0, 10000000, (1500000, 2))
    B = np.random.randint(0, 10000000, 50000)
    
    %timeit numba_approach(A, B)
    # 286 ms ± 16.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    # This is still 4 times faster than the fastest other approach!
    

    所以,你可以让它快一个数量级。 Numba不支持所有python / numpy功能(并不是所有功能都更快)但在这种情况下它已经足够了!

答案 1 :(得分:1)

使用set -  交集重新创建一个新的索引列表,其中[1, 2, 5]位于您的子列表中。 然后使用要删除的索引列表,使用集成在numpy中的np.delete()函数。

import numpy as np

A = np.array([[1,2],
              [2,4],
              [3,4],
              [4,5],
              [6,7]])

B = set([1, 2, 5])

indices_to_del = [i for i, sublist in enumerate(A) if B & set(sublist)]

C = np.delete(A, indices_to_del, 0)

print C
#[[3 4]
# [6 7]]

编辑

感谢@MSeifert我能够改进我的答案。

@ Ev.Kounis提出了另一个类似但更快的解决方案:

D = np.array([sublist for sublist in A if not B & set(sublist)])