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?


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.


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


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.


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.


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

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)

1
Dobry pomysł, ale dla uczciwego porównania powinieneś użyć rankdata(l, method='ordinal') - 1.
Warren Weckesser


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.


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źć ...


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]

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.