【问题标题】:What's the most performant way of removing elements in a NumPy array, based on the amount of times they appear in another array?根据元素出现在另一个数组中的次数,删除 NumPy 数组中元素的最高效方法是什么?
【发布时间】:2020-06-27 18:12:51
【问题描述】:

假设我有两个 Numpy 数组:

a = np.array([1,2,2,3,3,3])
b = np.array([2,2,3])

我想从a 中删除b 中的所有元素,它们在b 中出现的次数相同。即

diff(a, b)
>>> np.array([1,3,3])

请注意,对于我的用例,b 将始终是 a 的一个子集,并且两者都可能是无序的,但是像 numpy.setdiff1d 这样的类似集合的方法并没有削减它,因为删除每个是很重要的元素一定次数。

我目前的懒惰解决方案如下:

def diff(a, b):
    for el in b:
        idx = (el == a).argmax()
        if a[idx] == el:
            a = np.delete(a, idx)
    return a

但我想知道是否有更高效或更紧凑的“numpy-esque”编写方式?

【问题讨论】:

  • Ordered: 您可以 for 循环并切换掩码内的位,然后将该掩码应用于 numpy 数组。可能比使用 np.delete 重复重新创建数组快得多。
  • 数组需要订购吗?可以无序吗?还是你总是需要从左到右删除?
  • 无序: (1):np.unique 带有计数,然后使用其中的值重建一个数组。 (2):使用类似枚举的想法,将每个数组项与唯一索引配对,然后将集合差与结果。
  • @MateenUlhaq 当然,但最好我希望通过仅使用 numpy 来加速而不是诉诸 for 循环解决方案。如果可能的话。
  • @MateenUlhaq 也可能是无序的,更新了帖子。

标签: python arrays numpy


【解决方案1】:

这是基于np.searchsorted的矢量化方法-

import pandas as pd

def diff_v2(a, b):
    # Get sorted orders
    sidx = a.argsort(kind='stable')
    A = a[sidx]
    
    # Get searchsorted indices per sorted order
    idx = np.searchsorted(A,b)
    
    # Get increments
    s = pd.Series(idx)
    inc = s.groupby(s).cumcount().values
    
    # Delete elemnents off traced back positions
    return np.delete(a,sidx[idx+inc])

进一步优化

让我们在 groupby cumcount 部分使用 NumPy -

# Perform groupby cumcount on sorted array
def groupby_cumcount(idx):
    mask = np.r_[False,idx[:-1]==idx[1:],False]
    ids = mask[:-1].cumsum()
    count = np.diff(np.flatnonzero(~mask))
    return ids - np.repeat(ids[~mask[:-1]],count)

def diff_v3(a, b):
    # Get sorted orders
    sidx = a.argsort(kind='stable')
    A = a[sidx]
    
    # Get searchsorted indices per sorted order
    idx = np.searchsorted(A,b)
    
    # Get increments
    idx = np.sort(idx)
    inc = groupby_cumcount(idx)
    
    # Delete elemnents off traced back positions
    return np.delete(a,sidx[idx+inc])

基准测试

使用具有~2x 重复的10000 元素的设置aba 的一半大小。

In [52]: np.random.seed(0)
    ...: a = np.random.randint(0,5000,10000)
    ...: b = a[np.random.choice(len(a), 5000,replace=False)]

In [53]: %timeit diff(a,b)
    ...: %timeit diff_v2(a,b)
    ...: %timeit diff_v3(a,b)
108 ms ± 821 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
3.85 ms ± 53.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.89 ms ± 15.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

接下来,关于 100000 元素 -

In [54]: np.random.seed(0)
    ...: a = np.random.randint(0,50000,100000)
    ...: b = a[np.random.choice(len(a), 50000,replace=False)]

In [55]: %timeit diff(a,b)
    ...: %timeit diff_v2(a,b)
    ...: %timeit diff_v3(a,b)
4.45 s ± 20.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
37.5 ms ± 661 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
28 ms ± 122 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

对于正数和排序输出

我们可以使用np.bincount -

def diff_v4(a, b):
    C = np.bincount(a)
    C -= np.bincount(b,minlength=len(C))
    return np.repeat(np.arange(len(C)), C)

【讨论】:

  • 迎接挑战?
  • 顺便说一句,如果可以在这里询问,我尝试尝试 benchit 但在导入时它会因为抱怨 qtagg (IIRC) 后端而死掉。它必须是那个后端还是我可以使用不同的后端?
  • @PaulPanzer 你在用笔记本吗?另外,你是在导入benchit之前导入matplotlib吗?是的,时间已经足够接近了:)
  • 不,普通的python repl。是否导入matplotlib 似乎没有什么区别。
  • @PaulPanzer 如果你能导入benchit,你能给我输出benchit.print_specs()吗?是窗户吗?看来我需要为此做更多的测试。
【解决方案2】:

这是一种与@Divakar 类似但速度略快的方法(在撰写本文时,可能会发生变化......)。

import numpy as np

def pp():
    if a.dtype.kind == "i":
        small = np.iinfo(a.dtype).min
    else:
        small = -np.inf
    ba = np.concatenate([[small],b,a])
    idx = ba.argsort(kind="stable")
    aux = np.where(idx<=b.size,-1,1)
    aux = aux.cumsum()
    valid = aux==np.maximum.accumulate(aux)
    valid[0] = False
    valid[1:] &= valid[:-1]
    aux2 = np.zeros(ba.size,bool)
    aux2[idx[valid]] = True
    return ba[aux2.nonzero()]

def groupby_cumcount(idx):
    mask = np.r_[False,idx[:-1]==idx[1:],False]
    ids = mask[:-1].cumsum()
    count = np.diff(np.flatnonzero(~mask))
    return ids - np.repeat(ids[~mask[:-1]],count)

def diff_v3():
    # Get sorted orders
    sidx = a.argsort(kind='stable')
    A = a[sidx]
    
    # Get searchsorted indices per sorted order
    idx = np.searchsorted(A,b)
    
    # Get increments
    idx = np.sort(idx)
    inc = groupby_cumcount(idx)
    
    # Delete elemnents off traced back positions
    return np.delete(a,sidx[idx+inc])

np.random.seed(0)
a = np.random.randint(0,5000,10000)
b = a[np.random.choice(len(a), 5000,replace=False)]

from timeit import timeit

print(timeit(pp,number=100)*10)
print(timeit(diff_v3,number=100)*10)
print((pp() == diff_v3()).all())

np.random.seed(0)
a = np.random.randint(0,50000,100000)
b = a[np.random.choice(len(a), 50000,replace=False)]

print(timeit(pp,number=10)*100)
print(timeit(diff_v3,number=10)*100)
print((pp() == diff_v3()).all())

示例运行:

1.4644702401710674
1.6345531499246135
True
22.230969095835462
24.67835019924678
True

更新:@MateenUlhaq 的dedup_unique 的对应时间:

7.986748410039581
81.83312350302003

请注意,此函数产生的结果与 Divakar 和我的不同(至少不是微不足道的)。

【讨论】:

    【解决方案3】:

    你的方法

    def dedup_reference(a, b):
        for el in b:
            idx = (el == a).argmax()
            if a[idx] == el:
                a = np.delete(a, idx)
        return a
    

    扫描方法需要对输入进行排序:

    def dedup_scan(arr, sel):
        arr.sort()
        sel.sort()
        mask = np.ones_like(arr, dtype=np.bool)
        sel_idx = 0
        for i, x in enumerate(arr):
            if sel_idx == sel.size:
                break
            if x == sel[sel_idx]:
                mask[i] = False
                sel_idx += 1
        return arr[mask]
    

    np.unique计数方法

    def dedup_unique(arr, sel):
        d_arr = dict(zip(*np.unique(arr, return_counts=True)))
        d_sel = dict(zip(*np.unique(sel, return_counts=True)))
        d = {k: v - d_sel.get(k, 0) for k, v in d_arr.items()}
        res = np.empty(sum(d.values()), dtype=arr.dtype)
        idx = 0
        for k, count in d.items():
            res[idx:idx+count] = k
            idx += count
        return res
    

    您也许可以通过巧妙地使用 numpy 集合函数(例如 np.in1d)来完成与上述相同的操作,但我认为这并不比仅使用字典快。


    这是一个懒惰的基准测试尝试(更新为包括@Divakar 的diff_v2diff_v3 方法):

    >>> def timeit_ab(f, n=10):
    ...     cmd = f"{f}(a.copy(), b.copy())"
    ...     t = timeit(cmd, globals=globals(), number=n) / n
    ...     print("{:.4f} {}".format(t, f))
    
    >>> array_copy = lambda x, y: None
    
    >>> funcs = [
    ...     'array_copy',
    ...     'dedup_reference',
    ...     'dedup_scan',
    ...     'dedup_unique',
    ...     'diff_v2',
    ...     'diff_v3',
    ... ]
    
    >>> def run_test(maxval, an, bn):
    ...     global a, b
    ...     a = np.random.randint(maxval, size=an)
    ...     b = np.random.choice(a, size=bn, replace=False)
    ...     for f in funcs:
    ...         timeit_ab(f)
    
    >>> run_test(10**1, 10000, 5000)
    0.0000 array_copy
    0.0617 dedup_reference
    0.0035 dedup_scan
    0.0004 dedup_unique     (*)
    0.0020 diff_v2
    0.0009 diff_v3
    
    >>> run_test(10**2, 10000, 5000)
    0.0000 array_copy
    0.0643 dedup_reference
    0.0037 dedup_scan
    0.0007 dedup_unique     (*)
    0.0023 diff_v2
    0.0013 diff_v3
    
    >>> run_test(10**3, 10000, 5000)
    0.0000 array_copy
    0.0641 dedup_reference
    0.0041 dedup_scan
    0.0022 dedup_unique
    0.0027 diff_v2
    0.0016 diff_v3          (*)
    
    >>> run_test(10**4, 10000, 5000)
    0.0000 array_copy
    0.0635 dedup_reference
    0.0041 dedup_scan
    0.0082 dedup_unique
    0.0029 diff_v2
    0.0015 diff_v3          (*)
    
    >>> run_test(10**5, 10000, 5000)
    0.0000 array_copy
    0.0635 dedup_reference
    0.0041 dedup_scan
    0.0118 dedup_unique
    0.0031 diff_v2
    0.0016 diff_v3          (*)
    
    >>> run_test(10**6, 10000, 5000)
    0.0000 array_copy
    0.0627 dedup_reference
    0.0043 dedup_scan
    0.0126 dedup_unique
    0.0032 diff_v2
    0.0016 diff_v3          (*)
    

    要点:

    • dedup_reference 会随着重复次数的增加而显着减慢。
    • 如果值的范围很小,dedup_unique 最快。 diff_v3 非常快,并且不依赖于值的范围。
    • 数组复制时间可以忽略不计。
    • 字典很酷。

    性能特征在很大程度上取决于数据量(未测试)以及数据的统计分布。我建议使用您自己的数据测试这些方法并选择最快的方法。请注意,各种解决方案会产生不同的输出,并对输入做出不同的假设。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-04
      • 1970-01-01
      • 2011-04-09
      • 2021-10-25
      相关资源
      最近更新 更多