É possível usar o argsort em ordem decrescente?

181

Considere o seguinte código:

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

Isso me dá índices dos nmenores elementos. É possível usar esse mesmo argsortem ordem decrescente para obter os índices dos nelementos mais altos?

shn
fonte
3
Não é simplesmente ids = np.array(avgDists).argsort()[-n:]?
Jaime
2
@ Jaime: Não, isso não funciona. 'resposta certa' é [3, 1, 2]. Sua linha produz [2, 1, 3](se n == 3 como exemplo)
dawg
2
@drewk Bem, então faça isso ids = np.array(avgDists).argsort()[-n:][::-1]. O problema é evitar fazer uma cópia de toda a lista, que é o que você obtém quando adiciona uma -na frente dela. Não relevante para o pequeno exemplo do OP, poderia ser para casos maiores.
Jaime
1
@ Jaime: Você está certo. Veja minha resposta atualizada. A sintaxe tho é exatamente oposta ao seu comentário sobre a fatia final: np.array(avgDists).argsort()[::-1][:n]fará isso. Além disso, se você estiver usando um numpy, fique num numpy. Primeiro converter a lista para um array: avgDist=np.array(avgDists)então torna-seavgDist.argsort()[::-1][:n}
dawg

Respostas:

230

Se você negar uma matriz, os elementos mais baixos se tornam os elementos mais altos e vice-versa. Portanto, os índices dos nelementos mais altos são:

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

Outra maneira de raciocinar sobre isso, como mencionado nos comentários , é observar que os grandes elementos estão chegando por último no argsort. Portanto, você pode ler a partir do final do argsort para encontrar os nelementos mais altos:

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

Ambos os métodos são O (n log n) na complexidade do tempo, porque a argsortchamada é o termo dominante aqui. Mas a segunda abordagem tem uma boa vantagem: ela substitui uma negação de O (n) da matriz por uma fatia de O (1) . Se você estiver trabalhando com pequenas matrizes dentro de loops, poderá obter alguns ganhos de desempenho ao evitar essa negação e, se estiver trabalhando com grandes matrizes, poderá economizar no uso de memória, pois a negação cria uma cópia de toda a matriz.

Observe que esses métodos nem sempre fornecem resultados equivalentes: se uma implementação de classificação estável for solicitada argsort, por exemplo, passando o argumento de palavra-chave kind='mergesort', a primeira estratégia preservará a estabilidade da classificação, mas a segunda estratégia quebrará a estabilidade (ou seja, as posições de igual itens serão revertidos).

Exemplo de tempos:

Usando uma pequena variedade de 100 carros alegóricos e um comprimento de 30 cauda, ​​o método de visualização foi cerca de 15% mais 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 matrizes maiores, o argsort é dominante e não há diferença de tempo 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)

Observe que o comentário do nedim abaixo está incorreto. A truncagem antes ou depois da reversão não faz diferença na eficiência, pois essas duas operações estão apenas apresentando uma visão da matriz de maneira diferente e, na verdade, não estão copiando dados.

wim
fonte
14
É ainda mais eficiente cortar antes de reverter, ou seja,np.array(avgDists).argsort()[:-n][::-1]
nedim
3
Essas respostas não são equivalentes se a matriz original contiver nans. Nesse caso, a primeira solução parece dar o resultado mais natural com nans no final do que no começo.
Feilchenfeldt
1
Como eles se comparam quando uma classificação estável é desejada? Presumivelmente, a estratégia de fatiar reverte itens iguais?
Eric Eric
1
@ user3666197 Achei que não era relevante para a resposta. Se a negação cria uma cópia ou não (faz) não é realmente importante aqui, a informação relevante é que calcular a negação é O (n) complexidade versus tomar outra fatia que é O (1) .
Wim
1
@ user3666197 Sim, esse é um bom argumento - se uma matriz estiver consumindo 50% de memória disponível, certamente quereremos evitar copiá-la e causar troca. Vou editar novamente para mencionar que uma cópia é criada lá.
Wim
70

Assim como Python, isso [::-1]inverte a matriz retornada argsort()e [:n]fornece os últimos n elementos:

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

A vantagem desse método é que idsé uma visão do avgDists:

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

(O 'OWNDATA' sendo falso indica que esta é uma visualização, não uma cópia)

Outra maneira de fazer isso é algo como:

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

O problema é que a maneira como isso funciona é criar um negativo de cada elemento na matriz:

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

E cria uma cópia para fazer isso:

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

Portanto, se você cronometrar cada um, com este conjunto de dados muito pequeno:

>>> 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

O método view é substancialmente mais rápido (e usa 1/2 da memória ...)

dawg
fonte
4
Essa resposta é boa, mas acho que seu texto deturpa as características reais de desempenho: "mesmo com esse conjunto de dados muito pequeno, o método de visualização é substancialmente mais rápido" . Na realidade, a negação é O (n) e o argumento é O (n log n) . Isso significa que a discrepância de tempo diminuirá para conjuntos de dados maiores - o termo O (n log n) domina, no entanto, sua sugestão é uma otimização da parte O (n) . Portanto, a complexidade permanece a mesma, e é para esse pequeno conjunto de dados em particular que vemos diferenças significativas.
Wim
2
Complexidade assintoticamente equivalente ainda pode significar que um algoritmo é assintoticamente duas vezes mais rápido que outro. Descartar essas distinções pode ter consequências. Por exemplo, mesmo que a discrepância de tempo (em porcentagem) se aproxime de 0, eu estaria disposto a apostar que o algoritmo com negação ainda usa o dobro da memória.
Erro
@ bug Pode, mas não neste caso. Eu adicionei alguns horários à minha resposta. Os números mostram que, para matrizes maiores, essas abordagens têm tempos semelhantes, o que apóia a hipótese de que o argsort é dominante. Por negação, eu acho que você está certo sobre o uso da memória, mas os usuários ainda podem preferir que se preocupem com a posição das nan's e / ou precisem de uma classificação estável.
wim
6

Você pode usar os comandos flip numpy.flipud()ou numpy.fliplr()para obter os índices em ordem decrescente após a classificação usando o argsortcomando É o que eu costumo fazer.

Kanmani
fonte
Isso é muito mais lento que fatiar o stackoverflow.com/a/44921013/125507
endolith
5

Em vez de usar, np.argsortvocê pode usar np.argpartition- se você precisar apenas dos índices dos n elementos mais baixos / mais altos.

Isso não requer a classificação de toda a matriz, mas apenas a parte de que você precisa, mas observe que a "ordem dentro da sua partição" é indefinida; portanto, embora ele forneça os índices corretos, eles podem não ser pedidos corretamente:

>>> 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
fonte
Ou, se você estiver usando os dois juntos, ou seja, argsort e argpartition, a operação deverá ser executada na operação argpartition.
Demongolem
3

Você pode criar uma cópia da matriz e multiplicar cada elemento por -1.
Como efeito, os elementos anteriores maiores se tornariam os menores.
Os indeces dos n menores elementos da cópia são os n maiores elementos do original.

MentolBonbon
fonte
isso é feito negando facilmente a matriz, conforme declarado nas outras respostas:-array
onofricamila
2

Como o @Kanmani sugeriu, uma implementação mais fácil de interpretar pode ser usada numpy.flip, como no seguinte:

import numpy as np

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

Ao usar o padrão de visitante em vez de funções de membro, é mais fácil ler a ordem das operações.

Adam Erickson
fonte
1

Com o seu exemplo:

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

Obtenha índices de n valores máximos:

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

Classifique-os em ordem decrescente:

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

Obtenha resultados (para n = 4):

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

Outra maneira é usar apenas um '-' no argumento argsort como em: "df [np.argsort (-df [:, 0])]", desde que df seja o dataframe e você queira classificá-lo pela primeira vez coluna (representada pelo número da coluna '0'). Mude o nome da coluna conforme apropriado. Obviamente, a coluna deve ser numérica.

Biswajit Ghoshal
fonte