Obwohl es sich um eine numpy
Lösung handelt, habe ich mich entschlossen zu prüfen, ob es eine interessante numba
Lösung gibt. Und tatsächlich gibt es! Hier ist ein Ansatz, der die partitionierte Liste als zerlumptes Array darstellt, das in einem einzelnen vorab zugewiesenen Puffer gespeichert ist. Dies ist inspiriert von dem argsort
von Paul Panzer vorgeschlagenen Ansatz . (Eine ältere Version, die nicht so gut lief, aber einfacher war, siehe unten.)
@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)]
Dadurch wird eine Liste mit zehn Millionen Elementen in 75 ms verarbeitet. Dies entspricht einer fast 50-fachen Beschleunigung gegenüber einer in reinem Python geschriebenen listenbasierten Version.
Für eine langsamere, aber etwas besser lesbare Version hatte ich Folgendes zuvor, basierend auf der kürzlich hinzugefügten experimentellen Unterstützung für dynamisch dimensionierte "typisierte Listen", mit denen wir jeden Behälter viel schneller in einer nicht ordnungsgemäßen Weise füllen können.
Dies ringt numba
ein bisschen mit der Typ-Inferenz-Engine, und ich bin sicher, dass es einen besseren Weg gibt, mit diesem Teil umzugehen. Dies stellt sich auch als fast 10x langsamer als oben heraus.
@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
Ich habe diese gegen Folgendes getestet:
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()
Ich habe sie auch gegen eine vorkompilierte Cython-Version getestet, die der enum_bins_numba_buffer
(unten ausführlich beschriebenen) ähnelt .
Auf einer Liste von zehn Millionen zufälligen Ints ( ints = np.random.randint(0, 100, 10000000)
) erhalte ich die folgenden Ergebnisse:
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)
Beeindruckenderweise numba
übertrifft diese Art der Arbeit eine cython
Version derselben Funktion, selbst wenn die Grenzwertprüfung deaktiviert ist. Ich bin noch nicht vertraut genug pythran
, um diesen Ansatz damit zu testen, aber ich wäre an einem Vergleich interessiert. Aufgrund dieser Beschleunigung scheint es wahrscheinlich, dass die pythran
Version mit diesem Ansatz auch etwas schneller ist.
Hier ist die cython
Version als Referenz mit einigen Build-Anweisungen. Nach der cython
Installation benötigen Sie eine einfache setup.py
Datei wie die folgende:
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()]
)
Und das Cython-Modul 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)]
Führen Sie mit diesen beiden Dateien in Ihrem Arbeitsverzeichnis den folgenden Befehl aus:
python setup.py build_ext --inplace
Sie können die Funktion dann mit importieren from enum_bins_cython import enum_bins_cython
.
np.argsort([1, 2, 2, 0, 0, 1, 3, 5])
gibtarray([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64)
. dann können Sie einfach die nächsten Elemente vergleichen.