我正在尝试找到性能最佳的方法来从NumPy数组中查找唯一值。 NumPy的mounted: function() {
this.title = "Default Title";
this.titleCheck();
}
函数非常慢,在找到唯一值之前先对值进行排序。 Pandas使用klib C library对值进行哈希处理,速度要快得多。我正在寻找一个Cython解决方案。
最简单的解决方案似乎是遍历数组并使用Python集添加每个元素:
unique
我也试过c ++中的unordered_set
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
性能
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
所以大熊猫的速度比cythonized大约快2.5倍。当有更多不同的元素时,它的领先优势会增加令人惊讶的是,一个纯粹的python集(在列表中)击败了一个cythonized集。
我的问题 - 在Cython中有更快的方法,而不是反复使用# 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)
方法吗?并且可以改进c ++ unordered_set吗?
当我们使用unicode字符串时,故事会发生变化。我相信我必须将numpy数组转换为add
数据类型,以便为Cython正确添加其类型。
object
我再次尝试了来自c ++的@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
unordered_set
性能
创建一个包含1,000个不同值的100万个字符串数组
@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
因此,似乎Python的集合胜过unicode字符串的pandas而不是整数。再一次,在Cython中迭代数组并没有真正帮助我们。
如果你知道你的整数范围不是太疯狂,那么可以规避集合。你可以简单地创建一个全零的第二个数组/ 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)
,并在遇到每个零时转动它们的位置False
并将该数字附加到列表中。这是非常快的,因为没有进行散列。
以下适用于正整数数组。如果你有负整数,你必须添加一个常量来将数字移动到0。
True
缺点当然是内存使用,因为你的最大整数可能会强制一个非常大的数组。但是,如果您确切地知道每个数字有多少有效数字,这种方法也适用于浮点数。
整数50个独特的1,000,000总计
字符串1,000独特,总计1,000,000
感谢所有帮助加快这些速度。
答案 0 :(得分:7)
我认为你的答案是问题"找到独特元素的最快方法是什么?#34;是"它取决于"。这取决于您的数据集和硬件。
对于你的场景(我主要看整数情况)pandas(并使用khash
)做了相当不错的工作。我无法使用std::unordered_map
来匹配此效果。
然而,google::dense_hash_set
在我的实验中比熊猫解决方案略快。
请继续阅读以获得更详细的解释。
我想首先解释您正在观察的结果,然后再使用这些见解。
我从你的int-example开始:数组中只有50
个唯一元素,但是1,000,000
:
import numpy as np
import pandas as pd
a=np.random.randint(0,50, 10**6, dtype=np.int64)
作为我的机器np.unique()
和pd.unique()
的时间基准:
%timeit np.unique(a)
>>>82.3 ms ± 539 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit pd.unique(a)
>>>9.4 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
使用集合(O(n)
)的pandas方法比使用排序(O(nlogn)
)的numpy方法快10倍。 log n = 20
为n=10**6
,因此因子10与预期差异有关。
另一个区别是,np.unique
返回一个已排序的数组,因此可以使用二进制搜索来查找元素。 pd.unique
返回一个未排序的数组,因此我们需要对其进行排序(如果原始数据中没有多少重复项,则可能为O(n log n)
)或将其转换为类似集合的结构。
让我们来看看简单的Python-Set:
%timeit set(a)
>>> 257 ms ± 21.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我们必须首先注意这一点:我们正在比较苹果和橘子。先前的unique
- 函数返回numpy数组,它由低c-整数组成。这个返回一组完整的Python整数。完全不同的东西!
这意味着对于numpy-array中的每个元素,我们必须首先创建一个python-object - 相当大的开销,然后才能将它添加到集合中。
转换为Python整数可以在预处理步骤中完成 - 您的版本为list
:
A=list(a)
%timeit set(A)
>>> 104 ms ± 952 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit set(list(a))
>>> 270 ms ± 23.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
创建Python整数需要100多毫秒。但是,python-integers比低C-int更复杂,因此处理它们的成本更高。在C-int上使用pd.unique
而不是在Python-set中使用更快。
现在你的Cython版本:
%timeit unique_cython_int(a)
31.3 ms ± 630 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
我真的不明白。我希望它执行类似于set(a)
-cython将删除解释器,但这不能解释因子10.但是,我们只有50个不同的整数(甚至在整数池中,因为它们是小于256
),所以可能有一些优化,它起着作用/差异的作用。
让我们尝试另一个数据集(现在有10**5
个不同的数字):
b=np.random.randint(0, 10**5,10**6, dtype=np.int64)
%timeit unique_cython_int(b)
>>> 236 ms ± 31.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit set(b)
>>> 388 ms ± 15.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
小于2的加速速度是我所期待的。
让我们来看看cpp-version:
%timeit unique_cpp_int(a)
>>> 25.4 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit unique_cpp_int(b)
>>> 100 ms ± 4.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
将数据从cpp-set复制到Python集会有一些开销(正如DavidW所指出的那样),但是根据我的经验,我会期待这种行为:std::unordered_map
有点快比起Python,但不是最好的实现 - 熊猫似乎击败了它:
%timeit set(pd.unique(b))
>>> 45.8 ms ± 3.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
所以看起来,在这种情况下,有许多重复并且哈希函数很便宜,大熊猫解决方案很难被击败。
有人可能会尝试google data structures。
然而,当数据只有很少的重复时,numpy的排序解决方案可能会变得更快。主要原因是,numpy的unique
只需要内存的两倍 - 原始数据和输出,而pandas hash-set-solution需要更多的内存:原始数据,集合和输出。对于庞大的数据集,它可能会成为拥有足够RAM和没有足够RAM的区别。
这取决于set-implementation需要多少内存开销,而且总是关于内存和速度之间的权衡。例如,std::unordered_set
至少需要32
个字节来保存8
字节整数。一些谷歌的数据结构可以做得更好。
使用pandas / numpy unique运行/usr/bin/time -fpeak_used_memory:%M python check_mem.py
:
#check_mem.py
import numpy as np
import pandas as pd
c=np.random.randint(0, 2**63,5*10**7, dtype=np.int64)
#pd.unique(c)
np.unique(c)
显示numpy
为1.2 GB,pandas
为2.0 GB。
实际上,在我的Windows机器上np.unique
比pd.unique
更快,如果数组中只有(旁边)的唯一元素,即使对于"只有" 10^6
个元素(可能是因为使用的集合增长时所需的重新组合)。但是我的Linux机器不是这种情况。
pandas
不发光的另一种情况是散列函数的计算不便宜:考虑长字符串(让我们说1000
个字符)作为对象
要计算哈希值,需要考虑所有1000
个字符(这意味着很多数据 - >很多哈希未命中),两个字符串的比较主要是在一个或两个字符之后完成的 - 然后概率已经非常高,我们知道字符串是不同的。因此,numpy log n
的{{1}}因素看起来不再那么糟糕了。
在这种情况下,最好使用树集而不是哈希集。
改进cpp-unordered set:
使用cpp的无序集合的方法可以通过其方法unique
得到改进,这将消除重新散列的需要。但它并没有导入到cython中,所以Cython的使用非常繁琐。
然而,对于几乎所有元素都是唯一的数据,只有50个唯一元素和最多因子2(由于使用的调整大小策略而产生的摊销成本)的数据运行时不会产生任何影响。
reserve()
的哈希函数是身份(至少for gcc),所以在这里获得的不多(我不认为使用更花哨的哈希函数会有帮助)
我认为无法调整cpp的无序集以击败pandas使用的khash实现,这对于这类任务来说似乎相当不错。
以下是these pretty old benchmarks,其中显示ints
比khash
略快,只有google_dense更快。
使用谷歌密集地图:
在我的实验中,谷歌密集地图(来自here)能够击败std::unordered_map
- 基准代码可以在答案的最后找到。
如果只有50个独特元素,它会更快:
khash
但如果只有唯一的元素,也会更快:
#50 unique elements:
%timeit google_unique(a,r)
1.85 ms ± 8.26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit pd.unique(a)
3.52 ms ± 33.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
我的一些实验也表明,%timeit google_unique(c,r)
54.4 ms ± 375 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [3]: %timeit pd.unique(c)
75.4 ms ± 499 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
可能比khash使用更多的内存(最多20%),但需要更多的测试才能确定是否真的如此。
我不确定我的回答对你有什么帮助。我的外卖是:
google_hash_set
似乎是一个很好的起点。 google-tests的目录:
set(pd.unique(...))
相应的pyx文件:
#google_hash.cpp
#include <cstdint>
#include <functional>
#include <sparsehash/dense_hash_set>
typedef int64_t lli;
void cpp_unique(lli *input, int n, lli *output){
google::dense_hash_set<lli, std::hash<lli> > set;
set.set_empty_key(-1);
for (int i=0;i<n;i++){
set.insert(input[i]);
}
int cnt=0;
for(auto x : set)
output[cnt++]=x;
}
setup.py文件:
#google.pyx
cimport numpy as np
cdef extern from "google_hash.cpp":
void cpp_unique(np.int64_t *inp, int n, np.int64_t *output)
#out should have enough memory:
def google_unique(np.ndarray[np.int64_t,ndim=1] inp, np.ndarray[np.int64_t,ndim=1] out):
cpp_unique(&inp[0], len(inp), &out[0])
Ipython-benchmark脚本,在调用from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy as np
setup(ext_modules=cythonize(Extension(
name='google',
language='c++',
extra_compile_args=['-std=c++11'],
sources = ["google.pyx"],
include_dirs=[np.get_include()]
)))
之后:
python setup.py build_ext --inplace
其他商品
修复后的Cython版本:
import numpy as np
import pandas as pd
from google import google_unique
a=np.random.randint(0,50,10**6,dtype=np.int64)
b=np.random.randint(0, 10**5,10**6, dtype=np.int64)
c=np.random.randint(0, 2**63,10**6, dtype=np.int64)
r=np.zeros((10**6,), dtype=np.int64)
%timeit google_unique(a,r
%timeit pd.unique(a)
修复后的C ++版本:
%%cython
cimport cython
from numpy cimport ndarray
from cpython cimport set
cimport numpy as np
@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