检查python列表/ numpy ndarray中是否存在重复项的最快方法

时间:2018-06-15 22:55:15

标签: python arrays list numpy unique

我想确定我的列表(实际上是numpy.ndarray)是否在最快的执行时间内包含重复项。请注意,我不关心删除重复项,我只是想知道是否有任何重复项。

注意:如果这不是重复,我会感到非常惊讶,但我已尽力而为,找不到一个。最近的是this questionthis question,两者都要求返回唯一列表。

3 个答案:

答案 0 :(得分:4)

以下是我想到的四种方式。

TL; DR:如果你期望很少(少于1/1000)重复:

def contains_duplicates(X):
    return len(np.unique(X)) != len(X)

如果您希望频繁(超过1/1000)重复:

def contains_duplicates(X):
    seen = set()
    seen_add = seen.add
    for x in X:
        if (x in seen or seen_add(x)):
            return True
    return False

第一种方法是从this answer提前退出,它想要返回唯一值,第二种方法是应用于this answer的相同构思。

>>> import numpy as np
>>> X = np.random.normal(0,1,[10000])
>>> def terhorst_early_exit(X):
...:     elems = set()
...:     for i in X:
...:         if i in elems:
...:             return True
...:         elems.add(i)
...:     return False
>>> %timeit terhorst_early_exit(X)
100 loops, best of 3: 10.6 ms per loop
>>> def peterbe_early_exit(X):
...:     seen = set()
...:     seen_add = seen.add
...:     for x in X:
...:         if (x in seen or seen_add(x)):
...:             return True
...:     return False
>>> %timeit peterbe_early_exit(X)
100 loops, best of 3: 9.35 ms per loop
>>> %timeit len(set(X)) != len(X)
100 loops, best of 3: 4.54 ms per loop
>>> %timeit len(np.unique(X)) != len(X)
1000 loops, best of 3: 967 µs per loop

如果你从普通的Python列表开始,而不是numpy.ndarray

,事情就会改变
>>> X = X.tolist()
>>> %timeit terhorst_early_exit(X)
100 loops, best of 3: 9.34 ms per loop
>>> %timeit peterbe_early_exit(X)
100 loops, best of 3: 8.07 ms per loop
>>> %timeit len(set(X)) != len(X)
100 loops, best of 3: 3.09 ms per loop
>>> %timeit len(np.unique(X)) != len(X)
1000 loops, best of 3: 1.83 ms per loop

编辑:如果我们事先预期重复数量该怎么办?

上述比较是在假设a)可能没有重复,或b)我们更担心最坏情况而不是普通情况的情况下运作。

>>> X = np.random.normal(0, 1, [10000])
>>> for n_duplicates in [1, 10, 100]:
>>>     print("{} duplicates".format(n_duplicates))
>>>     duplicate_idx = np.random.choice(len(X), n_duplicates, replace=False)
>>>     X[duplicate_idx] = 0
>>>     print("terhost_early_exit")
>>>     %timeit terhorst_early_exit(X)
>>>     print("peterbe_early_exit")
>>>     %timeit peterbe_early_exit(X)
>>>     print("set length")
>>>     %timeit len(set(X)) != len(X)
>>>     print("numpy unique length")
>>>     %timeit len(np.unique(X)) != len(X)
1 duplicates
terhost_early_exit
100 loops, best of 3: 12.3 ms per loop
peterbe_early_exit
100 loops, best of 3: 9.55 ms per loop
set length
100 loops, best of 3: 4.71 ms per loop
numpy unique length
1000 loops, best of 3: 1.31 ms per loop
10 duplicates
terhost_early_exit
1000 loops, best of 3: 1.81 ms per loop
peterbe_early_exit
1000 loops, best of 3: 1.47 ms per loop
set length
100 loops, best of 3: 5.44 ms per loop
numpy unique length
1000 loops, best of 3: 1.37 ms per loop
100 duplicates
terhost_early_exit
10000 loops, best of 3: 111 µs per loop
peterbe_early_exit
10000 loops, best of 3: 99 µs per loop
set length
100 loops, best of 3: 5.16 ms per loop
numpy unique length
1000 loops, best of 3: 1.19 ms per loop

因此,如果您期望很少重复,那么numpy.unique功能就是您的选择。随着预期重复数量的增加,早期退出方法占主导地位。

答案 1 :(得分:2)

根据数组的大小以及重复的可能性,答案会有所不同。

例如,如果您希望平均数组有大约3个重复项,则提前退出会将平均时间(和空间)减少2 / 3rds;如果你希望1000个阵列中只有1个完全没有重复,那么只会增加一些复杂性而不会改进任何东西。

与此同时,如果数组足够大以至于构建一个与数组一样大的临时集可能会很昂贵,那么在它前面加上像bloom filter这样的概率测试可能会大大加快速度,但是如果没有,它再次只是浪费精力。

最后,如果可能的话,你想留在numpy。循环遍历一个浮点数组(或其他任何东西)并将每个浮点数装入Python对象中将花费几乎与散列和检查值一样多的时间,当然还要将内容存储在Python set中而不是优化numpy存储也很浪费。但是你必须将其与其他问题进行交换 - 你不能用numpy提前退出,并且可能有一个很好的C优化的布隆过滤器实现pip install,但不是任何numpy-友好。

因此,对于所有可能的情况,没有一个最佳解决方案。

只是想知道编写一个布隆过滤器是多么容易,这是我在几分钟内一起攻击的一个:

from bitarray import bitarray # pip3 install bitarray

def dupcheck(X):
    # Hardcoded values to give about 5% false positives for 10000 elements
    size = 62352
    hashcount = 4
    bits = bitarray(size)
    bits.setall(0)
    def check(x, hash=hash): # TODO: default-value bits, hashcount, size?
        for i in range(hashcount):
            if not bits[hash((x, i)) % size]: return False
        return True
    def add(x):
        for i in range(hashcount):
            bits[hash((x, i)) % size] = True
    seen = set()
    seen_add = seen.add
    for x in X:
        if check(x) or add(x):
            if x in seen or seen_add(x):
                return True
    return False

这仅使用12KB(62352位bitarray加上500浮点set)而不是80KB(10000浮点setnp.array)。当你只处理10K元素时,这并不重要,但是,如果使用超过一半物理RAM的10B元素,那将是另一回事。

当然,它几乎肯定会比使用np.unique或甚至set慢一个数量级或更慢,因为我们正在做所有慢速循环在Python中。但如果这值得做,那么在Cython中重写应该是轻而易举的(并且无需装箱和取消装箱即可直接访问numpy数组)。

答案 2 :(得分:0)

我的计时测试与Scott的小名单有所不同。使用python 3.7.3,对于randint(长度8)中的一个小numpy数组,set()的速度比np.unique要快,但是对于较大的数组(长度1000),set()的速度要快得多。

长度8

Timing test iterations: 10000
Function          Min      Avg Sec  Conclusion      p-value
----------  ---------  -----------  ------------  ---------
set_len     0          7.73486e-06  Baseline
unique_len  9.644e-06  2.55573e-05  Slower                0

长度1000

Timing test iterations: 10000
Function           Min      Avg Sec  Conclusion      p-value
----------  ----------  -----------  ------------  ---------
set_len     0.00011066  0.000270466  Baseline
unique_len  4.3684e-05  8.95608e-05  Faster                0

然后我尝试了自己的实现,但是我认为需要优化的C代码才能胜过设置:

def check_items(key_rand, **kwargs):
    for i, vali in enumerate(key_rand):
        for j in range(i+1, len(key_rand)):
            valj = key_rand[j]
            if vali == valj:
                break

长度8

Timing test iterations: 10000
Function            Min      Avg Sec  Conclusion      p-value
-----------  ----------  -----------  ------------  ---------
set_len      0           6.74221e-06  Baseline
unique_len   0           2.14604e-05  Slower                0
check_items  1.1138e-05  2.16369e-05  Slower                0

(使用我的easyinfo中的随机compare_time()函数)