Ranguj elementy w tablicy za pomocą Python / NumPy, bez dwukrotnego sortowania tablicy

100

Mam tablicę liczb i chciałbym utworzyć kolejną tablicę, która reprezentuje pozycję każdego elementu w pierwszej tablicy. Używam Pythona i NumPy.

Na przykład:

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

Oto najlepsza metoda, jaką wymyśliłem:

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

Czy istnieją lepsze / szybsze metody, które pozwalają uniknąć dwukrotnego sortowania tablicy?

joshayers
źródło
6
Twoja ostatnia linia jest odpowiednikiem ranks = temp.argsort().
Sven Marnach

Odpowiedzi:

67

Użyj krojenia po lewej stronie w ostatnim kroku:

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

Pozwala to uniknąć podwójnego sortowania poprzez odwrócenie permutacji w ostatnim kroku.

Sven Marnach
źródło
3
Perfekcyjnie, dziękuję! Wiedziałem, że istnieje rozwiązanie i wydawałoby się to oczywiste, kiedy je zobaczyłem. Zrobiłem trochę testów z timeit, a ta metoda jest nieco wolniejsza dla małych tablic. Na moim komputerze są równe, gdy tablica ma 2000 elementów. Przy 20000 elementach Twoja metoda jest około 25% szybsza.
joshayers
jakieś zalecenia, jak to zrobić wierszowo?
Xaser
Więcej niż 1 przyciemniony patrz odpowiedź poniżej.
mathtick
101

Użyj argsort dwukrotnie, najpierw w celu uzyskania kolejności tablicy, a następnie w celu uzyskania rankingu:

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

Kiedy mamy do czynienia z tablicami 2D (lub wyższymi wymiarami), należy przekazać argument osi do argsort, aby uporządkować właściwą oś.

k.rooijers
źródło
2
Zauważ, że jeśli liczby są powtarzane w twojej tablicy wejściowej (np. [4,2,7,1,1]), Dane wyjściowe [3,2,4,0,1]
uszeregują
4
Podwójne sortowanie jest nieefektywne. Odpowiedź @Sven Marnach pokazuje, jak osiągnąć ranking jednym wezwaniem do argsort.
Warren Weckesser
6
@WarrenWeckesser: Właśnie przetestowałem różnicę między nimi i masz rację w przypadku dużych tablic, ale dla wszystkiego mniejszego (n <100) podwójne sortowanie args jest szybsze (około 20% szybciej dla n = 100 i około 5 razy szybciej dla n = 10). Więc jeśli musisz zrobić wiele rankingów dla wielu małych zestawów wartości, ta metoda jest znacznie lepsza.
naught101
3
@WarrenWeckesser: Właściwie to się mylę, ta metoda jest zdecydowanie lepsza. Obie metody są również znacznie szybsze niż metoda scipy.stats. Wyniki: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101
1
@ naught101: W twoim skrypcie jest błąd. Linia array = np.random.rand(10)powinna być array = np.random.rand(n).
Warren Weckesser
88

To pytanie ma już kilka lat, a przyjęta odpowiedź jest świetna, ale myślę, że nadal warto wspomnieć o następujących. Jeśli nie masz nic przeciwko uzależnieniu od scipy, możesz użyć 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])

Fajną cechą programu rankdatajest to, że methodargument zapewnia kilka opcji obsługi powiązań. Na przykład istnieją trzy wystąpienia liczby 20 i dwa wystąpienia liczby 40 w b:

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

Domyślnie przypisuje średnią rangę do powiązanych wartości:

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

method='ordinal' przypisuje kolejne stopnie:

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

method='min' przypisuje minimalną rangę powiązanych wartości wszystkim powiązanym wartościom:

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

Więcej opcji można znaleźć w dokumentacji.

Warren Weckesser
źródło
1
tak, to najlepsza odpowiedź wszędzie tam, gdzie ważne są skrajne przypadki.
naught101
Uważam za interesujące, że rankdatawydaje się , że wykorzystuje ten sam mechanizm, co przyjęta odpowiedź, do wewnętrznego generowania wstępnego rankingu.
AlexV
5

Próbowałem rozszerzyć oba rozwiązania dla tablic A o więcej niż jednym wymiarze, zakładając, że przetwarzasz tablicę wiersz po wierszu (oś = 1).

Pierwszy kod rozszerzyłem o pętlę na wierszach; prawdopodobnie można to poprawić

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

A druga, idąc za sugestią k.rooijers, staje się:

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

Wygenerowałem losowo 400 tablic z kształtem (1000,100); pierwszy kod zajął około 7,5, drugi 3,8.

Igor Fobia
źródło
5

Zobacz wektoryzowaną wersję uśrednionej rangi poniżej. Uwielbiam np. Unikalne, naprawdę poszerza zakres tego, jaki kod można, a czego nie można efektywnie wektoryzować. Oprócz unikania pętli for w Pythonie, to podejście pozwala również uniknąć niejawnej podwójnej pętli nad „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
źródło
tak poza tym; Zrobiłem ten kod, aby wygenerować takie same dane wyjściowe, jak inny uśredniony kod rangi, ale mogę sobie wyobrazić, że minimalna ranga grupy powtarzających się liczb działa równie dobrze. Można to uzyskać jeszcze łatwiej, ponieważ >>> unique, index, inverse = np.unique (a, True, True) >>> rank_min = rank [index] [inverse]
Eelco Hoogendoorn
Otrzymuję następujący błąd z Twoim rozwiązaniem (numpy 1.7.1): AttributeError: obiekt „numpy.ufunc” nie ma atrybutu „at”
Strach
Wymaga to nowszej wersji numpy; twój jest dość stary
Eelco Hoogendoorn
4

Oprócz elegancji i krótkości rozwiązań pojawia się również kwestia wykonania. Oto mały punkt odniesienia:

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
źródło
1
Dobry pomysł, ale dla uczciwego porównania powinieneś użyć rankdata(l, method='ordinal') - 1.
Warren Weckesser
3

Użyj argsort () dwa razy, aby to zrobić:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])
Kwong
źródło
2
zostało to już wspomniane na
długo
2

Wypróbowałem powyższe metody, ale nie udało mi się, ponieważ miałem wiele zeores. Tak, nawet z pływakami zduplikowane elementy mogą być ważne.

Napisałem więc zmodyfikowane rozwiązanie 1D, dodając krok sprawdzania powiązania:

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

Uważam, że jest tak skuteczny, jak to tylko możliwe.

h2kyeong
źródło
0

Podobała mi się metoda autorstwa k.rooijers, ale jak napisał rcoup, powtarzające się liczby są uszeregowane według pozycji tablicy. To nie było dla mnie dobre, więc zmodyfikowałem wersję, aby postprocesować rangi i scalić wszystkie powtarzające się liczby w łączną średnią rangę:

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

Mam nadzieję, że to może pomóc również innym, próbowałem znaleźć inne rozwiązanie tego problemu, ale nie mogłem znaleźć ...

Martin F Thomsen
źródło
0

argsort i slice są operacjami symetrii.

spróbuj dwukrotnie wyciąć zamiast argsort dwa razy. ponieważ slice jest szybszy niż argsort

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = np.arange(array.shape[0])[order][order]
yupbank
źródło