¿Es posible usar argsort en orden descendente?

181

Considere el siguiente código:

avgDists = np.array([1, 8, 6, 9, 4])
ids = avgDists.argsort()[:n]

Esto me da índices de los nelementos más pequeños. ¿Es posible usar esto argsorten orden descendente para obtener los índices de los nelementos más altos?

shn
fuente
3
¿No es simplemente ids = np.array(avgDists).argsort()[-n:]?
Jaime
2
@Jaime: No, eso no funciona. 'respuesta correcta' es [3, 1, 2]. Su línea produce [2, 1, 3](si n == 3 como ejemplo)
dawg
2
@drewk Bueno, entonces hazlo ids = np.array(avgDists).argsort()[-n:][::-1]. La cuestión es evitar hacer una copia de toda la lista, que es lo que obtienes cuando agregas un -frente. No relevante para el pequeño ejemplo del OP, podría ser para casos más grandes.
Jaime
1
@Jaime: Tienes razón. Ver mi respuesta actualizada. La sintaxis aunque es justo opuesta a su comentario sobre el segmento final: np.array(avgDists).argsort()[::-1][:n]lo hará. Además, si vas a usar numpy, quédate en numpy. Primero convierta la lista a una matriz: avgDist=np.array(avgDists)luego se convierteavgDist.argsort()[::-1][:n}
dawg

Respuestas:

230

Si niega una matriz, los elementos más bajos se convierten en los elementos más altos y viceversa. Por lo tanto, los índices de los nelementos más altos son:

(-avgDists).argsort()[:n]

Otra forma de razonar sobre esto, como se menciona en los comentarios , es observar que los grandes elementos son los últimos en la clasificación. Entonces, puedes leer desde la cola del argsort para encontrar los nelementos más altos:

avgDists.argsort()[::-1][:n]

Ambos métodos son O (n log n) en complejidad temporal, porque la argsortllamada es el término dominante aquí. Pero el segundo enfoque tiene una buena ventaja: reemplaza una negación O (n) de la matriz con un segmento O (1) . Si está trabajando con matrices pequeñas dentro de bucles, entonces puede obtener algunas mejoras de rendimiento al evitar esa negación, y si está trabajando con matrices enormes, puede ahorrar en el uso de memoria porque la negación crea una copia de toda la matriz.

Tenga en cuenta que estos métodos no siempre dan resultados equivalentes: si se solicita una implementación de clasificación estable argsort, por ejemplo, pasando el argumento de la palabra clave kind='mergesort', entonces la primera estrategia preservará la estabilidad de clasificación, pero la segunda estrategia romperá la estabilidad (es decir, las posiciones de igual los artículos serán revertidos).

Tiempos de ejemplo:

Usando una pequeña matriz de 100 flotadores y una longitud de 30 colas, el método de visualización fue aproximadamente un 15% más rápido

>>> avgDists = np.random.rand(100)
>>> n = 30
>>> timeit (-avgDists).argsort()[:n]
1.93 µs ± 6.68 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> timeit avgDists.argsort()[::-1][:n]
1.64 µs ± 3.39 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> timeit avgDists.argsort()[-n:][::-1]
1.64 µs ± 3.66 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Para matrices más grandes, el argsort es dominante y no hay una diferencia de tiempo significativa

>>> avgDists = np.random.rand(1000)
>>> n = 300
>>> timeit (-avgDists).argsort()[:n]
21.9 µs ± 51.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> timeit avgDists.argsort()[::-1][:n]
21.7 µs ± 33.3 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> timeit avgDists.argsort()[-n:][::-1]
21.9 µs ± 37.1 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Tenga en cuenta que el comentario de nedim a continuación es incorrecto. Si se debe truncar antes o después de la inversión, no hay diferencia en la eficiencia, ya que ambas operaciones solo muestran una vista de la matriz de manera diferente y en realidad no copian datos.

wim
fuente
14
Es aún más eficiente cortar antes de invertir, es decir,np.array(avgDists).argsort()[:-n][::-1]
nedim
3
Estas respuestas no son equivalentes si la matriz original contiene nans. En tal caso, la primera solución parece dar el resultado más natural con nans al final en lugar de al principio.
feilchenfeldt
1
¿Cómo se comparan cuando se desea una ordenación estable? Presumiblemente, la estrategia de corte revierte elementos iguales?
Eric
1
@ user3666197 Sentí que no era relevante para la respuesta. Si la negación crea una copia o no (lo hace) no es realmente importante aquí, la información relevante es que calcular la negación es O (n) complejidad frente a tomar otra porción que es O (1) .
wim
1
@ user3666197 Sí, ese es un buen punto: si una matriz ocupa el 50% de la memoria disponible, sin duda querremos evitar copiarla y causar el intercambio. Editaré nuevamente para mencionar que allí se crea una copia.
wim
70

Al igual que Python, en eso [::-1]invierte la matriz devuelta por argsort()y [:n]da los últimos n elementos:

>>> avgDists=np.array([1, 8, 6, 9, 4])
>>> n=3
>>> ids = avgDists.argsort()[::-1][:n]
>>> ids
array([3, 1, 2])

La ventaja de este método es que idses una vista de avgDists:

>>> ids.flags
  C_CONTIGUOUS : False
  F_CONTIGUOUS : False
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False

(El 'OWNDATA' es Falso indica que esta es una vista, no una copia)

Otra forma de hacer esto es algo como:

(-avgDists).argsort()[:n]

El problema es que la forma en que esto funciona es crear negativos de cada elemento en la matriz:

>>> (-avgDists)
array([-1, -8, -6, -9, -4])

ANd crea una copia para hacerlo:

>>> (-avgDists_n).flags['OWNDATA']
True

Entonces, si cronometra cada uno, con este conjunto de datos muy pequeño:

>>> import timeit
>>> timeit.timeit('(-avgDists).argsort()[:3]', setup="from __main__ import avgDists")
4.2879798610229045
>>> timeit.timeit('avgDists.argsort()[::-1][:3]', setup="from __main__ import avgDists")
2.8372560259886086

El método de visualización es sustancialmente más rápido (y usa la mitad de la memoria ...)

perro
fuente
44
Esta respuesta es buena, pero creo que su redacción tergiversa las características reales de rendimiento: "incluso con este conjunto de datos muy pequeño, el método de visualización es sustancialmente más rápido" . En realidad, la negación es O (n) y el argumento es O (n log n) . Esto significa que la discrepancia en el tiempo disminuirá para conjuntos de datos más grandes: el término O (n log n) domina, sin embargo, su sugerencia es una optimización de la parte O (n) . Entonces, la complejidad sigue siendo la misma, y ​​es para este pequeño conjunto de datos en particular que vemos diferencias significativas.
wim
2
La complejidad asintóticamente equivalente todavía puede significar que un algoritmo es asintóticamente dos veces más rápido que otro. Desechar tales distinciones puede tener consecuencias. Por ejemplo, incluso si la discrepancia de tiempo (como porcentaje) se aproxima a 0, estaría dispuesto a apostar que el algoritmo con negación todavía usa el doble de memoria.
error
@bug Puede, pero no lo hace en este caso. He agregado algunos tiempos a mi respuesta. Los números muestran que para matrices más grandes estos enfoques tienen tiempos similares, lo que respalda la hipótesis de que el argumento es dominante. Para negarlo, supongo que tiene razón sobre el uso de la memoria, pero los usuarios aún pueden preferir eso si les importa la posición de los nan y / o necesitan un tipo estable.
wim
6

Puede usar los comandos flip numpy.flipud()o numpy.fliplr()para obtener los índices en orden descendente después de ordenarlos con el argsortcomando. Eso es lo que suelo hacer.

Kanmani
fuente
Eso es mucho más lento que cortar stackoverflow.com/a/44921013/125507
endolith el
5

En lugar de usar np.argsort, podría usar np.argpartition, si solo necesita los índices de los n elementos más bajos / más altos.

Eso no requiere ordenar toda la matriz, sino solo la parte que necesita, pero tenga en cuenta que el "orden dentro de su partición" no está definido, por lo que si bien proporciona los índices correctos, es posible que no estén ordenados correctamente:

>>> avgDists = [1, 8, 6, 9, 4]
>>> np.array(avgDists).argpartition(2)[:2]  # indices of lowest 2 items
array([0, 4], dtype=int64)

>>> np.array(avgDists).argpartition(-2)[-2:]  # indices of highest 2 items
array([1, 3], dtype=int64)
MSeifert
fuente
O, si está utilizando los dos juntos, es decir, argsort y argpartition, la operación debe realizarse en la operación argpartition.
demongolem
3

Puede crear una copia de la matriz y luego multiplicar cada elemento con -1.
Como efecto, los elementos más grandes anteriores se convertirían en los más pequeños.
Las indecesiones de los n elementos más pequeños en la copia son los n elementos más grandes en el original.

MentolBonbon
fuente
esto se hace fácilmente negando la matriz, como se indica en las otras respuestas:-array
onofricamila
2

Como insinuó @Kanmani, se puede usar una implementación más fácil de interpretar numpy.flip, como se muestra a continuación:

import numpy as np

avgDists = np.array([1, 8, 6, 9, 4])
ids = np.flip(np.argsort(avgDists))
print(ids)

Al usar el patrón de visitante en lugar de las funciones de miembro, es más fácil leer el orden de las operaciones.

Adam Erickson
fuente
1

Con tu ejemplo:

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

Obtenga índices de n valores máximos:

ids = np.argpartition(avgDists, -n)[-n:]

Clasifíquelos en orden descendente:

ids = ids[np.argsort(avgDists[ids])[::-1]]

Obtenga resultados (para n = 4):

>>> avgDists[ids]
array([9, 8, 6, 4])
Alexey Antonenko
fuente
-1

Otra forma es usar solo un '-' en el argumento para argsort como en: "df [np.argsort (-df [:, 0])]", siempre que df sea el marco de datos y desee ordenarlo por el primero columna (representada por el número de columna '0'). Cambie el nombre de la columna según corresponda. Por supuesto, la columna tiene que ser numérica.

Biswajit Ghoshal
fuente