我正试图找到最有效的方法来从NumPy数组中查找唯一的值。NumPy的unique
函数非常慢,并在查找唯一值之前对值进行排序。Pandas使用klib C库对值进行哈希处理,速度更快。我正在寻找一个Cython解决方案。
最简单的解决方案似乎就是遍历数组并使用Python集合添加每个元素,如下所示:
from numpy cimport ndarray
from cpython cimport set
@cython.wraparound(False)
@cython.boundscheck(False)
def unique_cython_int(ndarray[np.int64_t] a):
cdef int i
cdef int n = len(a)
cdef set s = set()
for i in range(n):
s.add(a[i])
return s
我也尝试了C++的unordered_set
from libcpp.unordered_set cimport unordered_set
@cython.wraparound(False)
@cython.boundscheck(False)
def unique_cpp_int(ndarray[np.int64_t] a):
cdef int i
cdef int n = len(a)
cdef unordered_set[int] s
for i in range(n):
s.insert(a[i])
return s
性能表现
# create array of 1,000,000
a = np.random.randint(0, 50, 1000000)
# Pure Python
%timeit set(a)
86.4 ms ± 2.58 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Convert to list first
a_list = a.tolist()
%timeit set(a_list)
10.2 ms ± 74.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# NumPy
%timeit np.unique(a)
32 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Pandas
%timeit pd.unique(a)
5.3 ms ± 257 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# Cython
%timeit unique_cython_int(a)
13.4 ms ± 1.02 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
# Cython - c++ unordered_set
%timeit unique_cpp_int(a)
17.8 ms ± 158 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
讨论
因此,pandas比经过Cython优化的集合快约2.5倍。当有更多不同元素时,它的领先优势会增加。令人惊讶的是,纯Python集合(在列表上)击败了Cython集合。
我的问题是,有没有比仅重复使用add
方法更快的Cython方法? C++的unordered_set是否可以改进?
使用Unicode字符串
当我们使用Unicode字符串时,情况就发生了变化。我认为我必须将numpy数组转换为object
数据类型,以便为Cython正确添加其类型。
@cython.wraparound(False)
@cython.boundscheck(False)
def unique_cython_str(ndarray[object] a):
cdef int i
cdef int n = len(a)
cdef set s = set()
for i in range(n):
s.add(a[i])
return s
我又尝试了来自c ++的unordered_set
@cython.wraparound(False)
@cython.boundscheck(False)
def unique_cpp_str(ndarray[object] a):
cdef int i
cdef int n = len(a)
cdef unordered_set[string] s
for i in range(n):
s.insert(a[i])
return s
性能
创建一个包含 1 百万个字符串的数组,其中有 1,000 个不同的值。
s_1000 = []
for i in range(1000):
s = np.random.choice(list('abcdef'), np.random.randint(5, 50))
s_1000.append(''.join(s))
s_all = np.random.choice(s_1000, 1000000)
# s_all has numpy unicode as its data type. Must convert to object
s_unicode_obj = s_all.astype('O')
# c++ does not easily handle unicode. Convert to bytes and then to object
s_bytes_obj = s_all.astype('S').astype('O')
# Pure Python
%timeit set(s_all)
451 ms ± 5.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit set(s_unicode_obj)
71.9 ms ± 5.91 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# using set on a list
s_list = s_all.tolist()
%timeit set(s_list)
63.1 ms ± 7.38 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# NumPy
%timeit np.unique(s_unicode_obj)
1.69 s ± 97.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit np.unique(s_all)
633 ms ± 3.99 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# Pandas
%timeit pd.unique(s_unicode_obj)
97.6 ms ± 6.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Cython
%timeit unique_cython_str(s_unicode_obj)
60 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Cython - c++ unordered_set
%timeit unique_cpp_str2(s_bytes_obj)
247 ms ± 8.45 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
讨论
因此,似乎Python的集合在Unicode字符串方面表现优于pandas,但在整数方面并非如此。另外,在Cython中遍历数组实际上对我们没有帮助。
使用整数欺骗
如果您知道整数范围不太复杂,则可以绕过集合。当遇到每个整数时,您可以简单地创建一个所有零/False
的第二个数组,并将它们的位置设置为True
并将该数字附加到列表中。由于不进行哈希处理,因此这非常快。
以下内容适用于正整数数组。如果您有负整数,则必须添加一个常量来将数字向上移动到0。
@cython.wraparound(False)
@cython.boundscheck(False)
def unique_bounded(ndarray[np.int64_t] a):
cdef int i, n = len(a)
cdef ndarray[np.uint8_t, cast=True] unique = np.zeros(n, dtype=bool)
cdef list result = []
for i in range(n):
if not unique[a[i]]:
unique[a[i]] = True
result.append(a[i])
return result
%timeit unique_bounded(a)
1.18 ms ± 21.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
当然,缺点是内存使用量,因为您的最大整数可能会强制使用非常大的数组。但是,如果您知道每个数字有多少个有效数字,这种方法也可以用于浮点数。
摘要
整数1000000个中有50个独特的
- Pandas-5毫秒
- Python集合列表-10毫秒
- Cython集合-13毫秒
- “作弊”整数-1.2毫秒
字符串1000000个中有1000个独特的
- Cython集合-60毫秒
- Python集合列表-63毫秒
- Pandas-98毫秒
感谢所有帮助加快速度的人。
set
使用与dict
相同的哈希处理方式,这是Python的核心处理组件。而列表迭代比数组迭代更快。因此,我并不惊讶Cython没有改进基本的Python处理过程。它没有为计算带来任何新东西。 - hpauljadd
设置方法是否最佳。我还想看看是否有其他哈希表实现可以产生更快的结果。而且也想知道“使用整数作弊”的策略是否可行。 - Ted Petrou