Classifica gli elementi in un array usando Python / NumPy, senza ordinare l'array due volte


100

Ho un array di numeri e vorrei creare un altro array che rappresenti il ​​rango di ogni elemento nel primo array. Sto usando Python e NumPy.

Per esempio:

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

Ecco il metodo migliore che ho escogitato:

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

Esistono metodi migliori / più veloci che evitano di ordinare due volte l'array?


6
La tua ultima riga è equivalente a ranks = temp.argsort().
Sven Marnach

Risposte:


67

Usa l'affettatura sul lato sinistro nell'ultimo passaggio:

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

Ciò evita l'ordinamento due volte invertendo la permutazione nell'ultimo passaggio.


3
Perfetto, grazie! Sapevo che c'era una soluzione e sarebbe sembrata ovvia una volta vista. Ho fatto alcuni test con timeit e questo metodo è leggermente più lento per i piccoli array. Sulla mia macchina sono uguali quando l'array ha 2.000 elementi. Con 20.000 elementi, il tuo metodo è circa il 25% più veloce.
joshayers

qualche consiglio su come eseguire questa operazione a righe?
Xaser

Per più di 1 dim vedi la risposta sotto.
mathtick

101

Usa argsort due volte, prima per ottenere l'ordine dell'array, poi per ottenere la classifica:

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

Quando si ha a che fare con array 2D (o di dimensioni superiori), assicurarsi di passare un argomento dell'asse ad argsort per ordinare sull'asse corretto.


2
Nota che se i numeri vengono ripetuti nell'array di input (ad es. [4,2,7,1,1]), L'output classifica quei numeri in base alla loro posizione dell'array ( [3,2,4,0,1])
rcoup

4
Ordinare due volte è inefficiente. La risposta di @Sven Marnach mostra come raggiungere la classifica con una sola chiamata a argsort.
Warren Weckesser

6
@ WarrenWeckesser: ho appena testato la differenza tra i due e hai ragione per array di grandi dimensioni, ma per qualsiasi cosa più piccola (n <100), il doppio argsort è più veloce (circa il 20% più veloce per n = 100 e circa 5 volte più veloce per n = 10). Quindi, se devi fare molte classifiche su molti piccoli gruppi di valori, questo metodo è molto migliore.
nought101

3
@ WarrenWeckesser: In realtà, mi sbaglio, questo metodo è decisamente migliore. Entrambi i metodi sono anche molto più veloci del metodo scipy.stats. Risultati: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101

1
@ naught101: c'è un bug nel tuo script. La linea array = np.random.rand(10)dovrebbe essere array = np.random.rand(n).
Warren Weckesser

88

Questa domanda è vecchia di pochi anni e la risposta accettata è ottima, ma penso che valga ancora la pena menzionare quanto segue. Se non ti dispiace la dipendenza da scipy, puoi usare 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])

Una bella caratteristica di rankdataè che l' methodargomento fornisce diverse opzioni per la gestione dei legami. Ad esempio, ci sono tre occorrenze di 20 e due occorrenze di 40 in b:

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

L'impostazione predefinita assegna il rango medio ai valori pari:

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

method='ordinal' assegna gradi consecutivi:

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

method='min' assegna il rango minimo dei valori legati a tutti i valori legati:

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

Vedere la docstring per ulteriori opzioni.


1
sì, questa è la risposta migliore ovunque siano importanti i casi limite.
nought101

Trovo interessante che rankdatasembri utilizzare lo stesso meccanismo della risposta accettata per generare internamente la classifica iniziale.
AlexV

5

Ho provato a estendere entrambe le soluzioni per gli array A di più di una dimensione, supponendo che tu elabori il tuo array riga per riga (asse = 1).

Ho esteso il primo codice con un ciclo su righe; probabilmente può essere migliorato

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

E il secondo, seguendo il suggerimento di k.rooijers, diventa:

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

Ho generato casualmente 400 array con forma (1000,100); il primo codice ha richiesto circa 7.5, il secondo 3.8.


5

Per una versione vettorializzata di un rango medio, vedi sotto. Adoro np.unique, amplia davvero l'ambito di ciò che il codice può e non può essere vettorializzato in modo efficiente. Oltre a evitare i cicli for di Python, questo approccio evita anche il doppio ciclo implicito su "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

a proposito; Ho creato questo codice per produrre lo stesso output dell'altro codice di rango medio, ma posso immaginare che il rango minimo di un gruppo di numeri ripetuti funzioni altrettanto bene. Questo può essere ottenuto ancora più facilmente come >>> unique, index, inverse = np.unique (a, True, True) >>> rank_min = rank [index] [inverse]
Eelco Hoogendoorn

Ricevo il seguente errore con la tua soluzione (numpy 1.7.1): AttributeError: l'oggetto 'numpy.ufunc' non ha attributo 'at'
Fear

Ciò richiede una versione più recente di numpy; il tuo è piuttosto antico
Eelco Hoogendoorn

4

Oltre all'eleganza e alla brevità delle soluzioni, c'è anche la questione delle prestazioni. Ecco un piccolo benchmark:

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
Buona idea, ma per un confronto equo, dovresti usare rankdata(l, method='ordinal') - 1.
Warren Weckesser,

3

Usa argsort () due volte lo farà:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])

2
questo è stato già menzionato molto prima che tu
posassi la

2

Ho provato i metodi di cui sopra, ma non è riuscito perché avevo molti zeores. Sì, anche con i float gli elementi duplicati possono essere importanti.

Quindi ho scritto una soluzione 1D modificata aggiungendo un passaggio di verifica del legame:

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

Credo che sia il più efficiente possibile.


0

Mi è piaciuto il metodo di k.rooijers, ma come ha scritto rcoup, i numeri ripetuti sono classificati in base alla posizione dell'array. Questo non andava bene per me, quindi ho modificato la versione per postelaborare i ranghi e unire i numeri ripetuti in un rango medio combinato:

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

Spero che questo possa aiutare anche gli altri, ho provato a trovare un'altra soluzione a questo, ma non sono riuscito a trovarne ...


0

argsort e slice sono operazioni di simmetria.

prova a tagliare due volte invece di argsort due volte. poiché slice è più veloce di argsort

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.