Czy można używać argsort w malejącej kolejności?

181

Rozważ następujący kod:

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

To daje mi wskaźniki nnajmniejszych elementów. Czy można użyć tego samego argsortw kolejności malejącej, aby uzyskać indeksy nnajwyższych elementów?

shn
źródło
3
Czy to nie jest po prostu ids = np.array(avgDists).argsort()[-n:]?
Jaime
2
@Jaime: Nie, to nie działa. „poprawna odpowiedź” brzmi [3, 1, 2]. Twoja linia produkuje [2, 1, 3](jeśli n == 3 jako przykład)
dawg
2
@drewk Cóż, a następnie zrób to ids = np.array(avgDists).argsort()[-n:][::-1]. Chodzi o to, aby uniknąć robienia kopii całej listy, co dostajesz po dodaniu -przed nią. Nie dotyczy to małego przykładu PO, może dotyczyć większych przypadków.
Jaime
1
@Jaime: Masz rację. Zobacz moją zaktualizowaną odpowiedź. Składnia tho jest dokładnie przeciwna do komentarza do końcowego fragmentu: np.array(avgDists).argsort()[::-1][:n]zrobi to. Ponadto, jeśli zamierzasz używać numpy, pozostań w numpy. Najpierw przekonwertuj listę na tablicę: avgDist=np.array(avgDists)potem staje sięavgDist.argsort()[::-1][:n}
dawg

Odpowiedzi:

230

Jeśli zanegujesz tablicę, najniższe elementy stają się najwyższymi elementami i odwrotnie. Dlatego wskaźniki nnajwyższych elementów to:

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

Innym sposobem uzasadnienia tego, jak wspomniano w komentarzach , jest zaobserwowanie, że duże elementy pojawiają się na końcu w argsort. Tak więc możesz czytać z ogona argsort, aby znaleźć nnajwyższe elementy:

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

Obie metody mają złożoność czasową O (n log n) , ponieważ argsortwywołanie jest tutaj terminem dominującym. Ale drugie podejście ma dobrą zaletę: zastępuje negację O (n) tablicy wycięciem O (1) . Jeśli pracujesz z małymi tablicami wewnątrz pętli, możesz uzyskać pewien wzrost wydajności dzięki unikaniu tej negacji, a jeśli pracujesz z dużymi tablicami, możesz zaoszczędzić na zużyciu pamięci, ponieważ negacja tworzy kopię całej tablicy.

Zauważ, że metody te nie zawsze dają równoważne wyniki: jeśli wymagana jest stabilna implementacja sortowania argsort, np. Poprzez przekazanie argumentu słowa kluczowegokind='mergesort' , wówczas pierwsza strategia zachowa stabilność sortowania, ale druga strategia złamie stabilność (tj. Pozycje równe przedmioty zostaną odwrócone).

Przykładowe czasy:

Przy użyciu małej tablicy 100 pływaków i ogona o długości 30 metoda widoku była o około 15% szybsza

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

W przypadku większych tablic dominuje argsort i nie ma znaczącej różnicy czasu

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

Uwaga: poniższy komentarz nedim jest nieprawidłowy. To, czy obcinać przed czy po cofnięciu, nie ma różnicy w wydajności, ponieważ obie te operacje jedynie zmieniają widok tablicy inaczej i nie kopiują danych.

wim
źródło
14
Jeszcze bardziej wydajne jest np.array(avgDists).argsort()[:-n][::-1]
krojenie
3
Te odpowiedzi nie są równoważne, jeśli oryginalna tablica zawiera nans. W takim przypadku pierwsze rozwiązanie wydaje się dawać bardziej naturalny wynik z nansami na końcu niż na początku.
feilchenfeldt
1
Jak je porównać, gdy pożądany jest stabilny sort? Prawdopodobnie strategia krojenia odwraca równe elementy?
Eric,
1
@ user3666197 Czułem, że to nie ma znaczenia dla odpowiedzi. To, czy negacja tworzy kopię, czy nie (nie robi), nie jest tutaj naprawdę ważne, istotną informacją jest to, że obliczenie negacji jest złożonością O (n) w porównaniu do pobrania innego wycinka, którym jest O (1) .
wim
1
@ user3666197 Tak, to dobra uwaga - jeśli tablica zajmuje 50% dostępnej pamięci, z pewnością będziemy chcieli uniknąć jej kopiowania i powodowania zamiany. Będę ponownie edytować, aby wspomnieć, że tam jest tworzona kopia.
wim
70

Podobnie jak Python, [::-1]odwraca tablicę zwracaną przez argsort()i [:n]daje ostatnie n elementów:

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

Zaletą tej metody jest to, że idsjest to widok z avgDists:

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

(„OWNDATA” to False oznacza, że ​​jest to widok, a nie kopia)

Innym sposobem na to jest coś takiego:

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

Problem polega na tym, że sposób ten polega na tworzeniu negatywu dla każdego elementu w tablicy:

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

I tworzy kopię, aby to zrobić:

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

Więc jeśli czas, każdy z tego bardzo małego zestawu danych:

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

Metoda przeglądania jest znacznie szybsza (i zajmuje 1/2 pamięci ...)

św
źródło
4
Ta odpowiedź jest dobra, ale czuję, że twoje sformułowania źle odzwierciedlają rzeczywistą charakterystykę wydajności: „nawet przy tym bardzo małym zestawie danych metoda wyświetlania jest znacznie szybsza” . W rzeczywistości negacją jest O (n), a argsort to O (n log n) . Oznacza to, że rozbieżność czasowa zmniejszy się dla większych zestawów danych - dominuje pojęcie O (n log n) , jednak twoja sugestia jest optymalizacją części O (n) . Tak więc złożoność pozostaje taka sama, a to dla tego małego zbioru danych , w szczególności , że widzimy żadnych znaczących różnic.
wim
2
Asymptotycznie równoważna złożoność może nadal oznaczać, że jeden algorytm jest asymptotycznie dwa razy szybszy niż inny. Wyrzucenie takich różnic może mieć konsekwencje. Na przykład, nawet jeśli rozbieżność czasu (w procentach) zbliża się do 0, byłbym skłonny założyć się, że algorytm z negacją nadal wykorzystuje dwa razy więcej pamięci.
błąd
@bug Może, ale w tym przypadku tak nie jest. Dodałem pewne czasy do mojej odpowiedzi. Liczby pokazują, że w przypadku większych tablic podejścia te mają podobne czasy, co potwierdza hipotezę, że argsort jest dominujący. Jeśli chodzi o negację, zgaduję, że masz rację co do wykorzystania pamięci, ale użytkownicy nadal wolą, jeśli zależy im na pozycji nan i / lub potrzebują stabilnego rodzaju.
wim
6

Możesz użyć poleceń odwracania numpy.flipud()lub numpy.fliplr()uzyskać indeksy w porządku malejącym po sortowaniu za pomocą argsortpolecenia. Tak zwykle robię.

Kanmani
źródło
To o wiele wolniej niż krojenie stackoverflow.com/a/44921013/125507
endolith
5

Zamiast używać np.argsortmożesz użyćnp.argpartition - jeśli potrzebujesz tylko indeksów najniższych / najwyższych n elementów.

Nie wymaga to sortowania całej tablicy, ale tylko potrzebnej części, ale należy zauważyć, że „porządek wewnątrz partycji” jest niezdefiniowany, więc chociaż daje prawidłowe indeksy, może nie być poprawnie uporządkowany:

>>> 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
źródło
Lub, jeśli używasz tych dwóch razem, tj. Argsort i argpartition, operację należy wykonać na operacji argpartition.
demongolem
3

Możesz utworzyć kopię tablicy, a następnie pomnożyć każdy element przez -1.
W efekcie pierwszeństwo przed największymi elementami byłyby najmniejsze.
Wersety n najmniejszych elementów w kopii są n największymi elementami w oryginale.

MentholBonbon
źródło
robi się to łatwo negując tablicę, jak stwierdzono w innych odpowiedziach:-array
onofricamila
2

Jak wskazał @Kanmani, można zastosować łatwiejszą do interpretacji implementację numpy.flip, jak poniżej:

import numpy as np

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

Używając wzorca gościa zamiast funkcji członka, łatwiej jest odczytać kolejność operacji.

Adam Erickson
źródło
1

Na przykład:

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

Uzyskaj indeksy n maksymalnych wartości:

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

Sortuj je w kolejności malejącej:

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

Uzyskaj wyniki (dla n = 4):

>>> avgDists[ids]
array([9, 8, 6, 4])
Aleksiej Antonienko
źródło
-1

Innym sposobem jest użycie tylko „-” w argumencie dla argumentu argsort, jak w: „df [np.argsort (-df [:, 0])]”, pod warunkiem, że df jest ramką danych i chcesz ją posortować według pierwszej kolumna (reprezentowana przez numer kolumny „0”). Zmień odpowiednio nazwę kolumny. Oczywiście kolumna musi być liczbowa.

Biswajit Ghoshal
źródło