Memberi peringkat item dalam array menggunakan Python / NumPy, tanpa mengurutkan array dua kali

100

Saya memiliki array angka dan saya ingin membuat array lain yang mewakili peringkat setiap item di array pertama. Saya menggunakan Python dan NumPy.

Sebagai contoh:

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

Inilah metode terbaik yang saya dapatkan:

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

Apakah ada metode yang lebih baik / lebih cepat yang menghindari pengurutan array dua kali?

joshayers
sumber
6
Baris terakhir Anda sama dengan ranks = temp.argsort().
Sven Marnach

Jawaban:

67

Gunakan pengiris di sisi kiri pada langkah terakhir:

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

Ini menghindari pengurutan dua kali dengan membalik permutasi pada langkah terakhir.

Sven Marnach
sumber
3
Sempurna, terima kasih! Saya tahu ada solusi dan akan terlihat jelas begitu saya melihatnya. Saya melakukan beberapa pengujian dengan timeit, dan metode ini sedikit lebih lambat untuk array kecil. Di mesin saya, keduanya sama ketika array memiliki 2.000 elemen. Pada 20.000 elemen, metode Anda sekitar 25% lebih cepat.
joshayer
ada rekomendasi tentang bagaimana melakukan ini secara berurutan?
Xaser
Untuk lebih dari 1 redup lihat jawaban di bawah.
mathtick
100

Gunakan argsort dua kali, pertama untuk mendapatkan urutan larik, lalu untuk mendapatkan peringkat:

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

Saat berhadapan dengan array 2D (atau dimensi yang lebih tinggi), pastikan untuk memberikan argumen sumbu ke argsort untuk diurutkan di atas sumbu yang benar.

k.rooijers
sumber
2
Perhatikan bahwa jika angka diulang dalam larik masukan Anda (misalnya [4,2,7,1,1]), keluaran akan memberi peringkat nomor tersebut berdasarkan posisi [3,2,4,0,1]
lariknya
4
Menyortir dua kali tidak efisien. Jawaban @Sven Marnach menunjukkan cara mencapai peringkat dengan satu panggilan ke argsort.
Warren Weckesser
6
@WarrenWeckesser: Saya baru saja menguji perbedaan antara keduanya, dan Anda tepat untuk array besar, tetapi untuk yang lebih kecil (n <100), argumen ganda lebih cepat (sekitar 20% lebih cepat untuk n = 100, dan sekitar 5 kali lebih cepat untuk n = 10). Jadi, jika Anda harus melakukan banyak peringkat pada banyak kumpulan nilai kecil, metode ini jauh lebih baik.
ada101
3
@WarrenWeckesser: Sebenarnya, saya salah, metode ini jauh lebih baik. Kedua metode tersebut juga jauh lebih cepat daripada metode scipy.stats. Hasil: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101
1
@ naught101: Ada bug dalam skrip Anda. Garis itu array = np.random.rand(10)seharusnya array = np.random.rand(n).
Warren Weckesser
88

Pertanyaan ini sudah berumur beberapa tahun, dan jawaban yang diterima bagus, tetapi saya pikir yang berikut ini masih layak untuk disebutkan. Jika Anda tidak keberatan dengan ketergantungan ini scipy, Anda dapat menggunakan 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])

Fitur yang bagus rankdataadalah bahwa methodargumen menyediakan beberapa opsi untuk menangani hubungan. Misalnya, ada tiga kemunculan 20 dan dua kemunculan 40 di b:

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

Default memberikan peringkat rata-rata ke nilai terikat:

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

method='ordinal' memberikan peringkat berturut-turut:

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

method='min' memberikan peringkat minimum dari nilai terikat ke semua nilai terikat:

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

Lihat docstring untuk opsi lebih lanjut.

Warren Weckesser
sumber
1
ya, ini adalah jawaban terbaik di mana pun di mana kasus edge penting.
naught101
Saya merasa menarik karena rankdatatampaknya menggunakan mekanisme yang sama dengan jawaban yang diterima untuk menghasilkan peringkat awal secara internal.
AlexV
5

Saya mencoba memperluas kedua solusi untuk array A lebih dari satu dimensi, misalkan Anda memproses array baris demi baris (axis = 1).

Saya memperpanjang kode pertama dengan loop pada baris; mungkin itu bisa diperbaiki

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

Dan yang kedua, mengikuti saran k.rooijers, menjadi:

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

Saya secara acak menghasilkan 400 array dengan bentuk (1000,100); kode pertama membutuhkan waktu sekitar 7,5, yang kedua 3,8.

Igor Fobia
sumber
5

Untuk versi vektor dari peringkat rata-rata, lihat di bawah. Saya suka np.unique, ini benar-benar memperluas cakupan kode apa yang dapat dan tidak dapat di-vektorisasi secara efisien. Selain menghindari loop-for python, pendekatan ini juga menghindari loop ganda implisit di atas '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
sumber
ngomong-ngomong; Saya membuat kode ini untuk menghasilkan output yang sama dengan kode peringkat rata-rata lainnya, tetapi saya dapat membayangkan peringkat minimum dari sekelompok angka berulang juga berfungsi dengan baik. Ini dapat diperoleh dengan lebih mudah seperti >>> unik, indeks, invers = np.unique (a, True, True) >>> rank_min = rank [index] [inverse]
Eelco Hoogendoorn
Saya mendapatkan kesalahan berikut dengan solusi Anda (numpy 1.7.1): AttributeError: objek 'numpy.ufunc' tidak memiliki atribut 'at'
Takut
Ini membutuhkan versi numpy yang lebih baru; milikmu cukup kuno
Eelco Hoogendoorn
4

Terlepas dari keanggunan dan singkatnya solusi, ada juga pertanyaan tentang kinerja. Ini sedikit patokannya:

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
sumber
1
Ide bagus, tetapi untuk perbandingan yang adil, Anda harus menggunakan rankdata(l, method='ordinal') - 1.
Warren Weckesser
3

Gunakan argsort () dua kali akan melakukannya:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])
Kwong
sumber
2
ini telah disebutkan jauh sebelum Anda mengajukan jawaban Anda
Ciprian Tomoiagă
2

Saya mencoba metode di atas, tetapi gagal karena saya memiliki banyak zeores. Ya, bahkan dengan float barang duplikat mungkin penting.

Jadi saya menulis solusi 1D yang dimodifikasi dengan menambahkan langkah pemeriksaan dasi:

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

Saya percaya ini seefisien mungkin.

h2kyeong
sumber
0

Saya menyukai metode oleh k.rooijers, tetapi seperti yang ditulis rcoup, angka yang diulang diberi peringkat sesuai dengan posisi array. Ini tidak baik bagi saya, jadi saya memodifikasi versinya untuk memproses peringkat dan menggabungkan angka berulang apa pun menjadi peringkat rata-rata gabungan:

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

Saya harap ini dapat membantu orang lain juga, saya mencoba menemukan solusi lain untuk ini, tetapi tidak dapat menemukan ...

Martin F Thomsen
sumber
0

argsort dan slice adalah operasi simetri.

coba iris dua kali, bukan argsort dua kali. karena slice lebih cepat dari argsort

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