Ответ 1
Я думаю, что ответ на ваш вопрос "какой самый быстрый способ найти уникальные элементы" - это "это зависит". Это зависит от вашего набора данных и от вашего оборудования.
Для ваших сценариев (я в основном рассматривал целочисленный случай) pandas (и используется khash
) делает довольно приличную работу. Я не смог сопоставить эту производительность с помощью std::unordered_map
.
Однако google::dense_hash_set
в моих экспериментах был немного быстрее, чем pandas -решение.
Пожалуйста, прочтите более подробное объяснение.
Я хотел бы начать с объяснения результатов, которые вы наблюдаете, и использовать эти идеи позже.
Я начинаю с вашего 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)
pandas подход с набором (O(n)
) примерно в 10 раз быстрее, чем метод numpy с сортировкой (O(nlogn)
). 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-массиве мы должны сначала создать объект python - достаточно накладные расходы, и только тогда мы можем добавить его в набор.
Преобразование в целые числа 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 более сложны, чем слабые C-int и, следовательно, их обработка стоит дороже. Использование pd.unique
в C-int и продвижение к Python-множеству происходит намного быстрее.
И теперь ваша версия 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-версию:
%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-набора в набор Python (как указывал DavidW), но в остальном поведение, как я ожидал бы, дало бы мой опыт: std::unordered_map
несколько быстрее, чем Python, но не самая большая реализация вокруг - panda, кажется, превзошла его:
%timeit set(pd.unique(b))
>>> 45.8 ms ± 3.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Итак, похоже, что в ситуации, когда есть много дубликатов, а хеш-функция дешевая, решение pandas трудно превзойти.
Возможно, вы можете попробовать структуры данных Google.
Однако, когда данные имеют только очень мало дубликатов, решение сортировки numpy может стать более быстрым. Основная причина заключается в том, что numpy unique
требуется только в два раза больше памяти - исходные данные и вывод, в то время как pandas хэш-набор-решение требует гораздо больше памяти: исходные данные, набор и вывод. Для огромных наборов данных это может стать разницей между достаточным объемом оперативной памяти и отсутствием достаточного объема оперативной памяти.
В зависимости от набора-реализации зависит от того, сколько оперативной памяти требуется, и это всегда связано с компромиссом между памятью и скоростью. Например, std::unordered_set
требуется, по крайней мере, 32
байт, чтобы сохранить целое число 8
-byte. Некоторые структуры данных google могут работать лучше.
Запуск /usr/bin/time -fpeak_used_memory:%M python check_mem.py
с pandas/numpy unique:
#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)
показывает 1,2 ГБ для numpy
и 2,0 ГБ для pandas
.
На самом деле, на моей машине с Windows np.unique
работает быстрее, чем pd.unique
, если в массиве есть (только рядом) только уникальные элементы, даже для элементов "only" 10^6
(возможно, из-за необходимых повторений в качестве используемый набор растет). Однако это не относится к моей машине Linux.
Другой сценарий, в котором pandas
не блистает, заключается в том, что вычисление хеш-функции не является дешевым: рассмотрите длинные строки (например, 1000
символы) как объекты.
Для вычисления хеш-значения нужно учитывать все 1000
символы (что означает, что много данных → много промахов хэша), сравнение двух строк в основном выполняется после одного или двух символов - вероятность тогда уже очень высока, мы знаем, что строки разные. Таким образом, коэффициент log n
numpy unique
больше не выглядит таким.
В этом случае лучше использовать древовидный набор вместо хеш-набора.
Улучшение в cpp-неупорядоченном наборе:
Метод, использующий неупорядоченный набор cpp, может быть улучшен благодаря его методу reserve()
, что избавит от необходимости переименования. Но он не импортируется на cython, поэтому использование довольно громоздко от Cython.
Однако резервирование не повлияло бы на время выполнения данных с использованием только 50 уникальных элементов и не более 2 (амортизируемые затраты из-за используемой стратегии изменения размера) для данных, имеющих почти все уникальные элементы.
Хеш-функция для ints
является тождеством (по крайней мере для gcc), поэтому не так много, чтобы получить здесь (я не знаю, t думаю, что использование более причудливой хэш-функции поможет здесь).
Я не вижу способа, чтобы неупорядоченный набор cpp мог быть изменен, чтобы превзойти реализацию khash, используемую pandas, что кажется довольно хорошим для этого типа задач.
Вот, например, эти довольно старые тесты, которые показывают, что khash
несколько быстрее, чем std::unordered_map
, причем только google_dense является четным Быстрее.
Использование плотной карты google:
В моих экспериментах гуманная карта google (от здесь) смогла победить khash
- в конце можно найти контрольный код ответа.
Это было быстрее, если было только 50 уникальных элементов:
#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)
Мои несколько экспериментов также показали, что google_hash_set
использует, возможно, больше памяти (до 20%), чем хаш, но требуется больше тестов, чтобы понять, действительно ли это так.
Я не уверен, что мой ответ помог вам вообще. Мои приемы:
- Если нам нужен набор Python-целых чисел,
set(pd.unique(...))
кажется хорошей отправной точкой. - Есть несколько случаев, когда решение сортировки numpy может быть лучше (меньше памяти, иногда хеш-расчет слишком дорог)
- Зная больше о данных, можно использовать для настройки решения, сделав лучший компромисс (например, используя меньше/больше памяти /preallocating, поэтому нам не нужно перефразировать или использовать битрейт для поиска).
- pandas решение, похоже, очень хорошо подходит для некоторых обычных случаев, но тогда для других случаев может быть и другой компромисс - google_dense является самым перспективным кандидатом.
Списки для google-тестов:
#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;
}
соответствующий pyx файл:
#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])
файл setup.py:
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()]
)))
Ipython-benchmark script, после вызова python setup.py build_ext --inplace
:
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)
Другие объявления
Версия Cython после исправлений:
%%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
Версия С++ после исправлений:
%%cython -+ -c=-std=c++11
cimport cython
cimport numpy as np
from numpy cimport ndarray
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