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?


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.


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.


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.


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.


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

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)

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

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.


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


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]

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.