Aquí hay un enfoque O (max (x) + len (x)) usando scipy.sparse
:
import numpy as np
from scipy import sparse
x = np.array("1 2 2 0 0 1 3 5".split(),int)
x
# array([1, 2, 2, 0, 0, 1, 3, 5])
M,N = x.max()+1,x.size
sparse.csc_matrix((x,x,np.arange(N+1)),(M,N)).tolil().rows.tolist()
# [[3, 4], [0, 5], [1, 2], [6], [], [7]]
Esto funciona creando una matriz dispersa con entradas en las posiciones (x [0], 0), (x [1], 1), ... Usando el CSC
formato (columna dispersa comprimida) esto es bastante simple. La matriz se convierte luego al LIL
formato (lista vinculada). Este formato almacena los índices de columna para cada fila como una lista en su rows
atributo, por lo que todo lo que tenemos que hacer es tomar eso y convertirlo a la lista.
Tenga en cuenta que las argsort
soluciones basadas en matrices pequeñas son probablemente más rápidas, pero en algunos de tamaño no increíblemente grande, esto se cruzará.
EDITAR:
argsort
basada solo en numpy
solución:
np.split(x.argsort(kind="stable"),np.bincount(x)[:-1].cumsum())
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]
Si el orden de los índices dentro de los grupos no importa, también puede intentarlo argpartition
(no hace ninguna diferencia en este pequeño ejemplo, pero esto no está garantizado en general):
bb = np.bincount(x)[:-1].cumsum()
np.split(x.argpartition(bb),bb)
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]
EDITAR:
@Divakar recomienda contra el uso de np.split
. En cambio, un bucle es probablemente más rápido:
A = x.argsort(kind="stable")
B = np.bincount(x+1).cumsum()
[A[B[i-1]:B[i]] for i in range(1,len(B))]
O puede utilizar el nuevo operador de morsa (Python3.8 +):
A = x.argsort(kind="stable")
B = np.bincount(x)
L = 0
[A[L:(L:=L+b)] for b in B.tolist()]
EDITAR (EDITADO):
(No puro numpy): como alternativa a numba (ver la publicación de @ senderle) también podemos usar pythran.
Compilar con pythran -O3 <filename.py>
import numpy as np
#pythran export sort_to_bins(int[:],int)
def sort_to_bins(idx, mx):
if mx==-1:
mx = idx.max() + 1
cnts = np.zeros(mx + 2, int)
for i in range(idx.size):
cnts[idx[i] + 2] += 1
for i in range(3, cnts.size):
cnts[i] += cnts[i-1]
res = np.empty_like(idx)
for i in range(idx.size):
res[cnts[idx[i]+1]] = i
cnts[idx[i]+1] += 1
return [res[cnts[i]:cnts[i+1]] for i in range(mx)]
Aquí numba
gana por un bigote en cuanto al rendimiento:
repeat(lambda:enum_bins_numba_buffer(x),number=10)
# [0.6235917090671137, 0.6071486569708213, 0.6096088469494134]
repeat(lambda:sort_to_bins(x,-1),number=10)
# [0.6235359431011602, 0.6264424560358748, 0.6217901279451326]
Cosas más antiguas:
import numpy as np
#pythran export bincollect(int[:])
def bincollect(a):
o = [[] for _ in range(a.max()+1)]
for i,j in enumerate(a):
o[j].append(i)
return o
Tiempos contra numba (antiguo)
timeit(lambda:bincollect(x),number=10)
# 3.5732191529823467
timeit(lambda:enumerate_bins(x),number=10)
# 6.7462647299980745
np.argsort([1, 2, 2, 0, 0, 1, 3, 5])
daarray([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64)
. entonces puedes comparar los siguientes elementos.