Хотя запрос на numpy
решение, я решил посмотреть, есть ли интересное numba
решение. И действительно, есть! Вот подход, который представляет разделенный список как рваный массив, хранящийся в одном предварительно выделенном буфере. Это черпает вдохновение из argsort
подхода, предложенного Полом Панцером . (Для более старой версии, которая не так хорошо, но была проще, см. Ниже.)
@numba.jit(numba.void(numba.int64[:],
numba.int64[:],
numba.int64[:]),
nopython=True)
def enum_bins_numba_buffer_inner(ints, bins, starts):
for x in range(len(ints)):
i = ints[x]
bins[starts[i]] = x
starts[i] += 1
@numba.jit(nopython=False) # Not 100% sure this does anything...
def enum_bins_numba_buffer(ints):
ends = np.bincount(ints).cumsum()
starts = np.empty(ends.shape, dtype=np.int64)
starts[1:] = ends[:-1]
starts[0] = 0
bins = np.empty(ints.shape, dtype=np.int64)
enum_bins_numba_buffer_inner(ints, bins, starts)
starts[1:] = ends[:-1]
starts[0] = 0
return [bins[s:e] for s, e in zip(starts, ends)]
Это обрабатывает список из десяти миллионов элементов за 75 мс, что почти в 50 раз быстрее по сравнению со списочной версией, написанной на чистом Python.
Для более медленной, но несколько более читаемой версии, вот что я имел раньше, основываясь на недавно добавленной экспериментальной поддержке «типизированных списков» с динамическим размером, которые позволяют нам гораздо быстрее заполнять каждую ячейку не по порядку.
Это numba
немного борется с механизмом вывода типа, и я уверен, что есть лучший способ справиться с этой частью. Это также оказывается почти в 10 раз медленнее, чем выше.
@numba.jit(nopython=True)
def enum_bins_numba(ints):
bins = numba.typed.List()
for i in range(ints.max() + 1):
inner = numba.typed.List()
inner.append(0) # An awkward way of forcing type inference.
inner.pop()
bins.append(inner)
for x, i in enumerate(ints):
bins[i].append(x)
return bins
Я проверил это в отношении следующего:
def enum_bins_dict(ints):
enum_bins = defaultdict(list)
for k, v in enumerate(ints):
enum_bins[v].append(k)
return enum_bins
def enum_bins_list(ints):
enum_bins = [[] for i in range(ints.max() + 1)]
for x, i in enumerate(ints):
enum_bins[i].append(x)
return enum_bins
def enum_bins_sparse(ints):
M, N = ints.max() + 1, ints.size
return sparse.csc_matrix((ints, ints, np.arange(N + 1)),
(M, N)).tolil().rows.tolist()
Я также проверил их на предварительно скомпилированной версии Cython, аналогичной enum_bins_numba_buffer
(подробно описанной ниже).
В списке из десяти миллионов случайных чисел ( ints = np.random.randint(0, 100, 10000000)
) я получаю следующие результаты:
enum_bins_dict(ints)
3.71 s ± 80.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_list(ints)
3.28 s ± 52.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_sparse(ints)
1.02 s ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_numba(ints)
693 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
enum_bins_cython(ints)
82.3 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
enum_bins_numba_buffer(ints)
77.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Впечатляет, что этот способ работы с numba
опережает cython
версию той же функции, даже с отключенной проверкой границ. У меня пока нет достаточных знаний, pythran
чтобы протестировать этот подход, используя его, но мне было бы интересно увидеть сравнение. Вероятно, исходя из этого ускорения,pythran
версия также может быть немного быстрее при таком подходе.
Вот cython
версия для справки, с некоторыми инструкциями по сборке. После cython
установки вам понадобится простой setup.py
файл, подобный следующему:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy
ext_modules = [
Extension(
'enum_bins_cython',
['enum_bins_cython.pyx'],
)
]
setup(
ext_modules=cythonize(ext_modules),
include_dirs=[numpy.get_include()]
)
И модуль Cython enum_bins_cython.pyx
:
# cython: language_level=3
import cython
import numpy
cimport numpy
@cython.boundscheck(False)
@cython.cdivision(True)
@cython.wraparound(False)
cdef void enum_bins_inner(long[:] ints, long[:] bins, long[:] starts) nogil:
cdef long i, x
for x in range(len(ints)):
i = ints[x]
bins[starts[i]] = x
starts[i] = starts[i] + 1
def enum_bins_cython(ints):
assert (ints >= 0).all()
# There might be a way to avoid storing two offset arrays and
# save memory, but `enum_bins_inner` modifies the input, and
# having separate lists of starts and ends is convenient for
# the final partition stage.
ends = numpy.bincount(ints).cumsum()
starts = numpy.empty(ends.shape, dtype=numpy.int64)
starts[1:] = ends[:-1]
starts[0] = 0
bins = numpy.empty(ints.shape, dtype=numpy.int64)
enum_bins_inner(ints, bins, starts)
starts[1:] = ends[:-1]
starts[0] = 0
return [bins[s:e] for s, e in zip(starts, ends)]
С этими двумя файлами в вашем рабочем каталоге выполните эту команду:
python setup.py build_ext --inplace
Затем вы можете импортировать функцию, используя from enum_bins_cython import enum_bins_cython
.
np.argsort([1, 2, 2, 0, 0, 1, 3, 5])
даетarray([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64)
. тогда вы можете просто сравнить следующие элементы.