Я пытаюсь найти наиболее эффективный метод для поиска уникальных значений из массива NumPy. Функция NumPy unique
работает очень медленно и сначала сортирует значения перед поиском уникальной. Pandas хэширует значения, используя klib C library, которая намного быстрее. Я ищу решение 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
Я также попробовал 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 примерно в 2,5 раза быстрее, чем набор cythonized. Его свинец увеличивается, когда есть более четкие элементы. Удивительно, но чистый набор python (в списке) превосходит набор cythonized.
Мой вопрос здесь - есть ли более быстрый способ сделать это в Cython, чем использовать метод add
повторно? А можно ли улучшить С++ 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
И снова я попробовал 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 миллиона строк с 1000 различными значениями
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 превосходит Pandas для строк unicode, но не для целых чисел. И снова, итерация через массив в Китоне действительно не помогает нам вообще.
Обман с целыми числами
Можно обойти множества, если вы знаете, что диапазон ваших целых чисел не слишком сумасшедший. Вы можете просто создать второй массив всех нулей / 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)
Недостатком является, конечно же, использование памяти, так как наибольшее целое число может привести к чрезвычайно большому массиву. Но этот метод мог бы работать и для float, если бы вы точно знали, сколько значительных цифр было у каждого номера.
Резюме
Целые числа 50 уникальных из 1 000 000 общих
- Pandas - 5 мс
- Набор Python списка - 10 мс
- Набор Cython - 13 мс
- "Обман" с целыми числами - 1,2 мс
Строки 1000 уникальные из 1 000 000 всего
- Комплект Cython - 60 мс
- Набор Python списка - 63 мс
- Pandas - 98 мс
Оцените всю помощь, сделав это быстрее.