Clasifique los elementos en una matriz usando Python / NumPy, sin ordenar la matriz dos veces

100

Tengo una matriz de números y me gustaría crear otra matriz que represente el rango de cada elemento en la primera matriz. Estoy usando Python y NumPy.

Por ejemplo:

array = [4,2,7,1]
ranks = [2,1,3,0]

Este es el mejor método que se me ocurrió:

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.arange(len(array))[temp.argsort()]

¿Existen métodos mejores / más rápidos que eviten ordenar la matriz dos veces?

joshayers
fuente
6
Tu última línea es equivalente a ranks = temp.argsort().
Sven Marnach

Respuestas:

67

Utilice el corte en el lado izquierdo en el último paso:

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.empty_like(temp)
ranks[temp] = numpy.arange(len(array))

Esto evita ordenar dos veces al invertir la permutación en el último paso.

Sven Marnach
fuente
3
¡Perfecto, gracias! Sabía que había una solución y parecería obvio una vez que la viera. Hice algunas pruebas con timeit, y este método es un poco más lento para arreglos pequeños. En mi máquina son iguales cuando la matriz tiene 2000 elementos. Con 20.000 elementos, su método es aproximadamente un 25% más rápido.
Joshayers
¿Alguna recomendación sobre cómo hacer esto por filas?
Xaser
Para más de 1 dim, consulte la respuesta a continuación.
mathtick
100

Use argsort dos veces, primero para obtener el orden de la matriz, luego para obtener la clasificación:

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = order.argsort()

Cuando se trata de matrices 2D (o de mayor dimensión), asegúrese de pasar un argumento de eje a argsort para ordenar sobre el eje correcto.

k.rooijers
fuente
2
Tenga en cuenta que si los números se repiten en su matriz de entrada (por ejemplo [4,2,7,1,1]), la salida clasificará esos números en función de su posición de matriz ( [3,2,4,0,1])
rcoup
4
Clasificar dos veces es ineficaz. La respuesta de @Sven Marnach muestra cómo lograr el ranking con una sola llamada a argsort.
Warren Weckesser
6
@WarrenWeckesser: Acabo de probar la diferencia entre los dos, y es adecuado para matrices grandes, pero para cualquier cosa más pequeña (n <100), el doble argsort es más rápido (aproximadamente un 20% más rápido para n = 100 y aproximadamente 5 veces más rápido para n = 10). Entonces, si tiene que hacer muchas clasificaciones en muchos conjuntos pequeños de valores, este método es mucho mejor.
naught101
3
@WarrenWeckesser: En realidad, estoy equivocado, este método es sin duda mejor. Ambos métodos también son mucho más rápidos que el método scipy.stats. Resultados: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101
1
@ naught101: Hay un error en su script. La línea array = np.random.rand(10)debería ser array = np.random.rand(n).
Warren Weckesser
88

Esta pregunta tiene algunos años y la respuesta aceptada es excelente, pero creo que vale la pena mencionar lo siguiente. Si no le importa la dependencia de scipy, puede usar scipy.stats.rankdata:

In [22]: from scipy.stats import rankdata

In [23]: a = [4, 2, 7, 1]

In [24]: rankdata(a)
Out[24]: array([ 3.,  2.,  4.,  1.])

In [25]: (rankdata(a) - 1).astype(int)
Out[25]: array([2, 1, 3, 0])

Una característica interesante de rankdataes que el methodargumento ofrece varias opciones para manejar los empates. Por ejemplo, hay tres ocurrencias de 20 y dos ocurrencias de 40 en b:

In [26]: b = [40, 20, 70, 10, 20, 50, 30, 40, 20]

El valor predeterminado asigna el rango promedio a los valores vinculados:

In [27]: rankdata(b)
Out[27]: array([ 6.5,  3. ,  9. ,  1. ,  3. ,  8. ,  5. ,  6.5,  3. ])

method='ordinal' asigna rangos consecutivos:

In [28]: rankdata(b, method='ordinal')
Out[28]: array([6, 2, 9, 1, 3, 8, 5, 7, 4])

method='min' asigna el rango mínimo de los valores vinculados a todos los valores vinculados:

In [29]: rankdata(b, method='min')
Out[29]: array([6, 2, 9, 1, 2, 8, 5, 6, 2])

Consulte la cadena de documentos para obtener más opciones.

Warren Weckesser
fuente
1
sí, esta es la mejor respuesta en cualquier lugar donde los casos extremos son importantes.
naught101
Me parece interesante que rankdataparezca usar el mismo mecanismo que la respuesta aceptada para generar la clasificación inicial internamente.
AlexV
5

Intenté extender ambas soluciones para matrices A de más de una dimensión, suponiendo que procesa su matriz fila por fila (eje = 1).

Extendí el primer código con un bucle en filas; probablemente se pueda mejorar

temp = A.argsort(axis=1)
rank = np.empty_like(temp)
rangeA = np.arange(temp.shape[1])
for iRow in xrange(temp.shape[0]): 
    rank[iRow, temp[iRow,:]] = rangeA

Y el segundo, siguiendo la sugerencia de k.rooijers, se convierte en:

temp = A.argsort(axis=1)
rank = temp.argsort(axis=1)

Generé aleatoriamente 400 matrices con forma (1000,100); el primer código tomó aproximadamente 7.5, el segundo 3.8.

Igor Fobia
fuente
5

Para obtener una versión vectorizada de un rango promedio, consulte a continuación. Me encanta np.unique, realmente amplía el alcance de lo que el código puede y no puede vectorizarse de manera eficiente. Además de evitar los bucles for de Python, este enfoque también evita el bucle doble implícito sobre 'a'.

import numpy as np

a = np.array( [4,1,6,8,4,1,6])

a = np.array([4,2,7,2,1])
rank = a.argsort().argsort()

unique, inverse = np.unique(a, return_inverse = True)

unique_rank_sum = np.zeros_like(unique)
np.add.at(unique_rank_sum, inverse, rank)
unique_count = np.zeros_like(unique)
np.add.at(unique_count, inverse, 1)

unique_rank_mean = unique_rank_sum.astype(np.float) / unique_count

rank_mean = unique_rank_mean[inverse]

print rank_mean
Eelco Hoogendoorn
fuente
por cierto; Hice este código para producir el mismo resultado que el otro código de rango promedio, pero puedo imaginar que el rango mínimo de un grupo de números repetidos funciona igual de bien. Esto se puede obtener aún más fácilmente como >>> único, índice, inverso = np.unique (a, Verdadero, Verdadero) >>> rango_min = rango [índice] [inverso]
Eelco Hoogendoorn
Recibo el siguiente error con su solución (numpy 1.7.1): AttributeError: el objeto 'numpy.ufunc' no tiene atributo 'at'
Fear
Esto requiere una versión más reciente de numpy; el tuyo es bastante antiguo
Eelco Hoogendoorn
4

Aparte de la elegancia y la brevedad de las soluciones, también está la cuestión del rendimiento. Aquí hay un pequeño punto de referencia:

import numpy as np
from scipy.stats import rankdata
l = list(reversed(range(1000)))

%%timeit -n10000 -r5
x = (rankdata(l) - 1).astype(int)
>>> 128 µs ± 2.72 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)

%%timeit -n10000 -r5
a = np.array(l)
r = a.argsort().argsort()
>>> 69.1 µs ± 464 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

%%timeit -n10000 -r5
a = np.array(l)
temp = a.argsort()
r = np.empty_like(temp)
r[temp] = np.arange(len(a))
>>> 63.7 µs ± 1.27 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)
Mischa Lisovyi
fuente
1
Buena idea, pero para una comparación justa, debería utilizar rankdata(l, method='ordinal') - 1.
Warren Weckesser
3

Use argsort () dos veces lo hará:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])
Kwong
fuente
2
esto ya se mencionó mucho antes de que planteara su respuesta
Ciprian Tomoiagă
2

Probé los métodos anteriores, pero fallé porque tenía muchos zeores. Sí, incluso con flotadores, los elementos duplicados pueden ser importantes.

Así que escribí una solución 1D modificada agregando un paso de verificación de empates:

def ranks (v):
    import numpy as np
    t = np.argsort(v)
    r = np.empty(len(v),int)
    r[t] = np.arange(len(v))
    for i in xrange(1, len(r)):
        if v[t[i]] <= v[t[i-1]]: r[t[i]] = r[t[i-1]]
    return r

# test it
print sorted(zip(ranks(v), v))

Creo que es lo más eficiente que puede ser.

h2kyeong
fuente
0

Me gustó el método de k.rooijers, pero como escribió rcoup, los números repetidos se clasifican según la posición de la matriz. Esto no fue bueno para mí, así que modifiqué la versión para posprocesar los rangos y fusionar los números repetidos en un rango promedio combinado:

import numpy as np
a = np.array([4,2,7,2,1])
r = np.array(a.argsort().argsort(), dtype=float)
f = a==a
for i in xrange(len(a)):
   if not f[i]: continue
   s = a == a[i]
   ls = np.sum(s)
   if ls > 1:
      tr = np.sum(r[s])
      r[s] = float(tr)/ls
   f[s] = False

print r  # array([ 3. ,  1.5,  4. ,  1.5,  0. ])

Espero que esto también ayude a otros, intenté encontrar otra solución para esto, pero no pude encontrar ninguna ...

Martin F Thomsen
fuente
0

argsort y slice son operaciones de simetría.

intente cortar dos veces en lugar de argsort dos veces. ya que slice es más rápido que argsort

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = np.arange(array.shape[0])[order][order]
yupbank
fuente