Meskipun permintaan adalah numpy
solusi, saya memutuskan untuk melihat apakah ada numba
solusi berbasis- menarik . Dan memang ada! Berikut adalah pendekatan yang mewakili daftar dipartisi sebagai array kasar yang disimpan dalam satu buffer yang dialokasikan sebelumnya. Ini mengambil beberapa inspirasi dari argsort
pendekatan yang diusulkan oleh Paul Panzer . (Untuk versi yang lebih lama yang tidak melakukannya juga, tetapi lebih sederhana, lihat di bawah.)
@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)]
Ini memproses daftar sepuluh juta item dalam 75ms, yang hampir 50x percepatan dari versi berbasis daftar yang ditulis dengan Python murni.
Untuk versi yang lebih lambat tetapi agak lebih mudah dibaca, inilah yang saya miliki sebelumnya, berdasarkan pada dukungan eksperimental yang baru-baru ini ditambahkan untuk "daftar yang diketik," yang berukuran dinamis yang memungkinkan kami mengisi setiap nampan dengan cara yang tidak sesuai pesanan jauh lebih cepat.
Ini sedikit bergulat dengan numba
tipe mesin inferensi, dan saya yakin ada cara yang lebih baik untuk menangani bagian itu. Ini juga ternyata hampir 10x lebih lambat dari yang di atas.
@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
Saya menguji ini terhadap yang berikut:
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()
Saya juga mengujinya terhadap versi cython yang dikompilasi mirip dengan enum_bins_numba_buffer
(dijelaskan secara rinci di bawah).
Pada daftar sepuluh juta int acak ( ints = np.random.randint(0, 100, 10000000)
) saya mendapatkan hasil berikut:
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)
Secara mengesankan, cara ini bekerja dengan numba
mengungguli cython
versi dari fungsi yang sama, bahkan dengan memeriksa batas dimatikan. Saya belum memiliki cukup keakraban pythran
untuk menguji pendekatan ini menggunakannya, tetapi saya akan tertarik untuk melihat perbandingan. Tampaknya berdasarkan pada percepatan ini bahwa pythran
versi mungkin juga sedikit lebih cepat dengan pendekatan ini.
Inilah cython
versi untuk referensi, dengan beberapa instruksi pembuatan. Setelah Anda cython
menginstal, Anda akan memerlukan setup.py
file sederhana seperti ini:
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()]
)
Dan modul 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)]
Dengan dua file ini di direktori kerja Anda, jalankan perintah ini:
python setup.py build_ext --inplace
Anda kemudian dapat mengimpor fungsi menggunakan from enum_bins_cython import enum_bins_cython
.
np.argsort([1, 2, 2, 0, 0, 1, 3, 5])
memberiarray([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64)
. maka Anda bisa membandingkan elemen berikutnya.