Ранжуйте елементи в масиві за допомогою Python / NumPy, не сортуючи масив двічі


100

У мене є масив чисел, і я хотів би створити інший масив, який представляє ранг кожного елемента в першому масиві. Я використовую Python та NumPy.

Наприклад:

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

Ось найкращий метод, який я придумав:

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

Чи є кращі / швидші методи, які уникають сортування масиву двічі?


6
Ваш останній рядок еквівалентний ranks = temp.argsort().
Свен Марнах

Відповіді:


67

На останньому кроці використовуйте нарізку ліворуч:

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

Це дозволяє уникнути сортування двічі шляхом інвертування перестановки на останньому кроці.


3
Ідеально, дякую! Я знав, що є рішення, і здавалося б очевидним, коли я його побачив. Я робив тестування з timeit, і цей метод трохи повільніше для невеликих масивів. На моїй машині вони рівні, коли масив має 2000 елементів. З 20000 елементами ваш метод на 25% швидший.
Joshayers

будь-які рекомендації щодо того, як це зробити роуіз?
Xaser

Більше 1 тьму див. Відповідь нижче.
mathtick

100

Використовуйте argsort двічі, спочатку, щоб отримати порядок масиву, а потім отримати рейтинг:

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

Маючи справу з двовимірними (або більш розмірними) масивами, обов'язково передайте аргумент осі argsort для впорядкування по правильній осі.


2
Зауважте, що якщо числа повторюються у вхідному масиві (напр. [4,2,7,1,1]), Вихід буде ранжувати ці числа, виходячи з їх положення масиву ( [3,2,4,0,1])
rcoup

4
Сортування двічі неефективне. @Sven Marnach відповідь показує, як досягти рейтингу за допомогою одного дзвінка до argsort.
Warren Weckesser

6
@WarrenWeckesser: Я щойно перевірив різницю між двома, і ти маєш рацію для великих масивів, але для чого-небудь меншого (n <100) подвійний аргурт швидше (приблизно на 20% швидше для n = 100 і приблизно в 5 разів швидше при n = 10). Тож якщо вам доведеться робити багато ранжирувань за великою кількістю малих наборів значень, цей метод набагато кращий.
naught101

3
@WarrenWeckesser: Насправді я помиляюсь, цей метод краще переносити руки. Обидва методи набагато швидші, ніж метод scipy.stats. Результати: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101

1
@ naught101: У вашому сценарії є помилка. Лінія array = np.random.rand(10)повинна бути array = np.random.rand(n).
Warren Weckesser

88

Це питання кілька років, і прийнята відповідь чудова, але я думаю, що наступне все ж варто згадати. Якщо ви не заперечуєте проти залежності scipy, ви можете використовувати 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])

Приємною особливістю rankdataє те, що methodаргумент надає кілька варіантів обробки зв’язків. Наприклад, є три випадки 20 і два випадки 40 у b:

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

За замовчуванням присвоюється середній ранг прив’язаним значенням:

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

method='ordinal' присвоює послідовні звання:

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

method='min' присвоює мінімальний ранг прив'язаних значень усім прив’язаним значенням:

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

Докладніші параметри див. У докстрині.


1
так, це найкраща відповідь у будь-якому місці, де важливі крайові справи.
naught101

Мені rankdataздається цікавим те, що, здається, використовується той же механізм, що і прийнята відповідь, щоб генерувати початковий рейтинг внутрішньо.
AlexV

5

Я намагався розширити обидва рішення для масивів A більш ніж одного виміру, припускаючи, що ви обробляєте масив рядок за рядком (ось = 1).

Я розширив перший код за допомогою циклу на рядах; ймовірно, це можна вдосконалити

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

А другий, за пропозицією k.rooijers, стає:

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

Я випадково генерував 400 масивів з формою (1000,100); перший код зайняв близько 7,5, другий 3,8.


5

Векторизовану версію усередненого рангу див. Нижче. Я люблю np.unique, він дійсно розширює сферу застосування коду, а не може бути ефективно векторизованим. Окрім уникнення python for-циклів, цей підхід також дозволяє уникнути подвійного подвійного циклу над '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

до речі; Я зробив цей код для отримання такого ж результату, що й інший усереднений код рангу, але я можу уявити, що мінімальний ранг групи повторюваних чисел так само добре працює. Це можна отримати ще простіше, оскільки >>> унікальний, індекс, зворотний = np.unique (a, True, True) >>> rank_min = rank [індекс] [зворотний]
Eelco Hoogendoorn

Я отримую таку помилку з вашим рішенням (numpy 1.7.1): AttributeError: 'numpy.ufunc' об’єкт не має атрибута 'at'
страх

Для цього потрібна остання версія numpy; ваш досить давній
Eelco Hoogendoorn

4

Окрім елегантності та короткості рішень, існує ще й питання продуктивності. Ось невеликий орієнтир:

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
Гарна ідея, але для справедливого порівняння вам слід скористатися rankdata(l, method='ordinal') - 1.
Warren Weckesser


2

Я спробував вищевказані методи, але не вдався, тому що у мене було багато завдань. Так, навіть з поплавцями повторювані елементи можуть бути важливими.

Тому я написав модифіковане рішення 1D, додавши крок перевірки зв'язання:

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

Я вважаю, що це настільки ефективно, наскільки це може бути.


0

Мені сподобався метод k.rooijers, але, як писав rcoup, повторні числа класифікуються відповідно до положення масиву. Це було недобре для мене, тому я змінив версію, щоб переробляти ранги та об'єднувати будь-які повторювані номери в комбінований середній рейтинг:

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

Я сподіваюся, що це може допомогти і іншим, я намагався знайти інше рішення для цього, але не зміг знайти жодного ...


0

аргорт і зріз - це операції симетрії.

спробуйте скибочку двічі замість аргурту двічі. так як зріз швидше аргсорта

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

0

Більш загальна версія однієї з відповідей:

In [140]: x = np.random.randn(10, 3)

In [141]: i = np.argsort(x, axis=0)

In [142]: ranks = np.empty_like(i)

In [143]: np.put_along_axis(ranks, i, np.repeat(np.arange(x.shape[0])[:,None], x.shape[1], axis=1), axis=0)

Див. Як використовувати numpy.argsort () в якості індексів для більш ніж двох вимірів? узагальнити до більшої кількості дим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.