我有一个可以是任何类型的2D NumPy数组,但是对于这个例子,我们可以假设它是整数。我希望找到找到阵列中所有唯一行的最快方法。
我最初的策略是将每一行转换为元组并将其添加到集合中。如果集合的长度增加,则意味着找到了唯一的行。
我不知道该怎么做才能快速将每一行哈希为字节。有一个问题entire array is hashed here。
我尝试了什么 - 元组创建
创建元组有很多种方法,每种方法都会影响性能。这是我的功能,我展示了4种不同的变体:
def unique_int_tuple1(ndarray[np.int64_t, ndim=2] a):
cdef int i, len_before
cdef int nr = a.shape[0]
cdef int nc = a.shape[1]
cdef set s = set()
cdef ndarray[np.uint8_t, cast = True] idx = np.zeros(nr, dtype='bool')
for i in range(nr):
len_before = len(s)
s.add(tuple(a[i])) # THIS LINE IS CHANGED FOR ALL VERSIONS
if len(s) > len_before:
idx[i] = True
return idx
s.add(tuple([a[i, j] for j in range(nc)]))
vals
是一个长度等于列数的列表
for j in range(nc):
vals[j] = a[i, j]
s.add(tuple(vals))
s.add((a[i, 0], a[i, 1], a[i, 2], a[i, 3]))
a = np.random.randint(0, 8, (10**5, 4))
%timeit unique_int_tuple1(a)
125 ms ± 1.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit unique_int_tuple2(a)
14.5 ms ± 93.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit unique_int_tuple3(a)
11.7 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit unique_int_tuple4(a)
9.59 ms ± 108 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
避免使用tuple
构造函数(版本4)可以获得良好的性能提升。
tostring
从上面链接的SO问题中,我可以在每一行上使用tostring
方法,然后将其哈希。
def unique_int_tostring(ndarray[np.int64_t, ndim=2] a):
cdef int i, j
cdef int nr = a.shape[0]
cdef int nc = a.shape[1]
cdef set s = set()
cdef ndarray[np.uint8_t, cast = True] idx = np.zeros(nr, dtype='bool')
for i in range(nr):
len_before = len(s)
s.add(a[i].tostring())
if len(s) > len_before:
idx[i] = True
return idx
这有效,但速度很慢:
%timeit unique_int_tostring(a)
40 ms ± 428 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
我认为,减速的很大一部分是每行a[i]
的访问权限。我们可以使用类型化的内存视图来提高性能,但我不知道如何将类型化的内存视图元素转换为字符串,以便对它们进行哈希处理。
def unique_int_memoryview(long[:, :] a):
cdef int i, j
cdef int nr = a.shape[0]
cdef int nc = a.shape[1]
cdef set s = set()
for i in range(nr):
s.add(<SOMETHING>) # NO IDEA HERE
return s
答案 0 :(得分:2)
这对我来说很惊讶,但是对于它的价值而言,这是一个c ++解决方案,可以完成你所指向的 - 将每行散列为一组字节。 “技巧”是获取元素<char*>&a[i, 0]
的地址 - 其他大部分内容都是记账。
对于不同的哈希表impl,我可能会做一些显然不太理想和/或性能更好。
编辑:
re:如何从一行创建一个字符串
我认为你能做的最好的就是从指针构造一个bytes
对象。这必然涉及行的副本,请参阅c api docs。
%%cython
from numpy cimport *
cimport numpy as np
import numpy as np
from cpython.bytes cimport PyBytes_FromStringAndSize
def unique_int_string(ndarray[np.int64_t, ndim=2] a):
cdef int i, len_before
cdef int nr = a.shape[0]
cdef int nc = a.shape[1]
cdef set s = set()
cdef ndarray[np.uint8_t, cast = True] idx = np.zeros(nr, dtype='bool')
cdef bytes string
for i in range(nr):
len_before = len(s)
string = PyBytes_FromStringAndSize(<char*>&a[i, 0], sizeof(np.int64_t) * nc)
s.add(string)
if len(s) > len_before:
idx[i] = True
return idx
//计时
In [9]: from unique import unique_ints
In [10]: %timeit unique_int_tuple4(a)
100 loops, best of 3: 10.1 ms per loop
In [11]: %timeit unique_ints(a)
100 loops, best of 3: 11.9 ms per loop
In [12]: (unique_ints(a) == unique_int_tuple4(a)).all()
Out[12]: True
// helper.h
#include <unordered_set>
#include <cstring>
struct Hasher {
size_t size;
size_t operator()(char* buf) const {
// https://github.com/yt-project/yt/blob/c1569367c6e3d8d0a02e10d0f3d0bd701d2e2114/yt/utilities/lib/fnv_hash.pyx
size_t hash_val = 2166136261;
for (int i = 0; i < size; ++i) {
hash_val ^= buf[i];
hash_val *= 16777619;
}
return hash_val;
}
};
struct Comparer {
size_t size;
bool operator()(char* lhs, char* rhs) const {
return (std::memcmp(lhs, rhs, size) == 0) ? true : false;
}
};
struct ArraySet {
std::unordered_set<char*, Hasher, Comparer> set;
ArraySet (size_t size) : set(0, Hasher{size}, Comparer{size}) {}
ArraySet () {}
bool add(char* buf) {
auto p = set.insert(buf);
return p.second;
}
};
// unique.pyx
from numpy cimport int64_t, uint8_t
import numpy as np
cdef extern from 'helper.h' nogil:
cdef cppclass ArraySet:
ArraySet()
ArraySet(size_t)
bint add(char*)
def unique_ints(int64_t[:, :] a):
cdef:
Py_ssize_t i, nr = a.shape[0], nc = a.shape[1]
ArraySet s = ArraySet(sizeof(int64_t) * nc)
uint8_t[:] idx = np.zeros(nr, dtype='uint8')
bint found;
for i in range(nr):
found = s.add(<char*>&a[i, 0])
if found:
idx[i] = True
return idx
// setup.py
from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy as np
exts = [
Extension('unique', ['unique.pyx'], language='c++', include_dirs=[np.get_include()])
]
setup(name='test', ext_modules=cythonize(exts))
答案 1 :(得分:2)
您可以使用ndarray.view()
将dtype
更改为byte string
,然后使用pandas.Series.duplicated()
查找重复的行:
import numpy as np
a = np.random.randint(0, 5, size=(200, 3))
s = pd.Series(a.view(("S", a[0].nbytes))[:, 0])
s.duplicated()
duplicated()
的核心算法在Cython中实现。但是,它需要将原始数组转换为对象数组,这可能会很慢。
要跳过object array
,您可以直接使用Pandas使用的khash library,这是C代码:
#include "khash.h"
typedef struct _Buf{
unsigned short n;
char * pdata;
} Buf;
khint32_t kh_buf_hash_func(Buf key)
{
int i;
char * s;
khint32_t hash = 0;
s = key.pdata;
for(i=0;i<key.n;i++)
{
hash += *s++;
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return hash;
}
khint32_t kh_buf_hash_equal(Buf a, Buf b)
{
int i;
if(a.n != b.n) return 0;
for(i=0;i<a.n;i++){
if(a.pdata[i] != b.pdata[i]) return 0;
}
return 1;
}
KHASH_INIT(buf, Buf, char, 0, kh_buf_hash_func, kh_buf_hash_equal)
void duplicated(char * arr, int row_size, int count, char * res)
{
kh_buf_t * khbuf;
Buf row;
int i, absent;
khint_t k;
row.n = row_size;
khbuf = kh_init_buf();
kh_resize_buf(khbuf, 4 * count);
for(i=0;i<count;i++){
row.pdata = &arr[i * row_size];
k = kh_put_buf(khbuf, row, &absent);
if (absent){
res[i] = 0;
}
else{
res[i] = 1;
}
}
kh_destroy_buf(khbuf);
}
然后用Cython或Ctypes或cffi包装duplicated()
函数。