Есть ли лучший / более быстрый способ вычислить относительный ранг каждого элемента в массиве? - PullRequest
2 голосов
/ 28 апреля 2020

Я хочу вычислить относительный ранг каждого элемента в массиве среди элементов перед ним. Например, в массиве [2,1,4,3] относительный ранг (от малого до большого) второго элемента (1) среди подмножества массива [2,1] равен 1. Относительный ранг третьего элемента (4) среди подмножества массива [2,1,4] равен 3. Окончательный относительный ранг каждого элемента должен быть [1,1,3,3].

Я использую следующий python код:

x = np.array([2,1,4,3])
rr = np.ones(4)
for i in range(1,4):
    rr[i] = sum(x[i] >= x[:i+1])

Есть ли другие более быстрые способы?

Ответы [ 5 ]

1 голос
/ 28 апреля 2020

Вот векторизованный способ с broadcasting -

n = len(x)
m1 = x[1:,None]>=x
m2 = np.tri(n-1,n,k=1, dtype=bool)
rr[1:] = (m1 & m2).sum(1)

В качестве альтернативы, мы могли бы ввести einsum или np.matmul, чтобы сделать последний шаг sum-reduction -

(m1.astype(np.float32)[:,None,:] @ m2[:,:,None])[:,0,0]
np.einsum('ij,ij->i',m1.astype(np.float32),m2)
1 голос
/ 29 апреля 2020

Ваш текущий алгоритм занимает квадратичное c время, которое не будет масштабироваться до больших входов. Вы можете сделать это намного лучше.

Один из способов сделать это лучше - использовать отсортированную структуру данных, например sortedcontainers.SortedList, и выполнить серию поисков и вставок. В следующем примере реализации возвращается список, предполагается, что нет связей, и значения рангов начинаются с 0:

import sortedcontainers

def rank(nums):
    sortednums = sortedcontainers.SortedList()
    ranks = []
    for num in nums:
        ranks.append(sortednums.bisect_left(num))
        sortednums.add(num)
    return ranks

Большая часть работы выполняется внутри реализации SortedList, а SortedList довольно быстро, поэтому это не следует не слишком много Python накладных расходов. Наличие sortedcontainers определенно делает это более удобным, чем следующий параметр, если не обязательно более эффективным.

Этот параметр выполняется за ... O (n log n) -i sh время. SortedList использует двухслойную иерархию вместо традиционной древовидной структуры, что делает преднамеренный компромисс между большим перемещением данных и меньшей погоней за указателями, поэтому теоретически вставка не является O (log n), но она эффективна на практике.


Следующим вариантом будет использование расширенной сортировки слиянием. Если вы сделаете это, вы захотите использовать Numba или Cython, потому что вам придется писать циклы вручную.

Основная идея c состоит в том, чтобы выполнить сортировку слиянием, но с отслеживанием ранга. каждого элемента в его подмассиве, как вы go. При объединении двух отсортированных подмассивов каждый элемент с левой стороны сохраняет свой старый ранг, в то время как значения ранга для элементов с правой стороны корректируются в сторону увеличения на количество элементов слева, которое было меньше их.

Это опция запускается в O (n log n).

Неоптимизированная реализация, работающая со списками Python, предполагающая отсутствие связей и начальные ранги в 0, будет выглядеть следующим образом:

def rank(nums):
    _, indexes, ranks = _augmented_mergesort(nums)
    result = [None]*len(nums)
    for i, rank_ in zip(indexes, ranks):
        result[i] = rank_
    return result

def _augmented_mergesort(nums):
    # returns sorted nums, indexes of sorted nums in original nums, and corresponding ranks
    if len(nums) == 1:
        return nums, [0], [0]
    left, right = nums[:len(nums)//2], nums[len(nums)//2:]
    return _merge(*_augmented_mergesort(left), *_augmented_mergesort(right))

def _merge(lnums, lindexes, lranks, rnums, rindexes, rranks):
    nums, indexes, ranks = [], [], []
    i_left = i_right = 0

    def add_from_left():
        nonlocal i_left
        nums.append(lnums[i_left])
        indexes.append(lindexes[i_left])
        ranks.append(lranks[i_left])
        i_left += 1
    def add_from_right():
        nonlocal i_right
        nums.append(rnums[i_right])
        indexes.append(rindexes[i_right] + len(lnums))
        ranks.append(rranks[i_right] + i_left)
        i_right += 1

    while i_left < len(lnums) and i_right < len(rnums):
        if lnums[i_left] < rnums[i_right]:
            add_from_left()
        elif lnums[i_left] > rnums[i_right]:
            add_from_right()
        else:
            raise ValueError("Tie detected")

    if i_left < len(lnums):
        nums += lnums[i_left:]
        indexes += lindexes[i_left:]
        ranks += lranks[i_left:]
    else:
        while i_right < len(rnums):
            add_from_right()

    return nums, indexes, ranks

Для оптимизированной реализации вам понадобится базовый вариант сортировки вставок, вы захотите использовать Numba или Cython, вам захочется работать с массивами, и вы не захотите делать так много выделения.

1 голос
/ 28 апреля 2020

Не уверен, что это быстрее, но вы можете сделать это с помощью понимания списка, которое всегда скрасит мой день:

[sorted(x[:i+1]).index(v)+1 for i, v in enumerate(x)]
0 голосов
/ 07 мая 2020

Когда я преобразовал все алгоритмы в numpy 2D массив, я обнаружил, что мой алгоритм является лучшим. Конечно, производительность также зависит от размера двумерного массива. Но 380х900 это мой случай. Я думаю, что Numpy вычисление массива очень полезно. Вот коды:

import numpy as np
import time
import sortedcontainers
def John(x): #x is 1D array
    n=len(x)
    rr=[]
    for i in range(n):
        rr.append(np.sum(x[i]>=x[:i+1]))
    return np.array(rr)

def John_2D(rv): #rv is 2d numpy array. rank it along axis 1!
    nr,nc=rv.shape
    rr=[]
    for i in range(nc):
        rr.append(np.sum((rv[:,:i+1]<=rv[:,i:i+1]),axis=1))
    return np.array(rr).T

def Matvei(x): #x is 1D array
    return [sorted(x[:i+1]).index(v)+1 for i, v in enumerate(x)]

def Divarkar1(x):#x is 1D array
    n = len(x)
    rr=np.ones(n,dtype=int)
    m1 = x[1:,None]>=x
    m2 = np.tri(n-1,n,k=1, dtype=bool)
    rr[1:] = (m1 & m2).sum(1)
    return rr

def Divarkar2(x):#x is 1D array
    n = len(x)
    rr=np.ones(n,dtype=int)
    m1 = x[1:,None]>=x
    m2 = np.tri(n-1,n,k=1, dtype=bool)
    (m1.astype(np.float32)[:,None,:] @ m2[:,:,None])[:,0,0]
    rr[1:]=np.einsum('ij,ij->i',m1.astype(np.float32),m2)
    return rr

def Monica1(nums): #nums is 1D array
    sortednums = sortedcontainers.SortedList()
    ranks = []
    for num in nums:
        ranks.append(sortednums.bisect_left(num))
        sortednums.add(num)
    return np.array(ranks)+1

def Monica2(nums): #nums is 1D array
    _, indexes, ranks = _augmented_mergesort(nums)
    result = [None]*len(nums)
    for i, rank_ in zip(indexes, ranks):
        result[i] = rank_
    return np.array(result)+1

def _augmented_mergesort(nums): #nums is 1D array
    # returns sorted nums, indexes of sorted nums in original nums, and corresponding ranks
    if len(nums) == 1:
        return nums, [0], [0]
    left, right = nums[:len(nums)//2], nums[len(nums)//2:] #split the array by half
    return _merge(*_augmented_mergesort(left), *_augmented_mergesort(right))

def _merge(lnums, lindexes, lranks, rnums, rindexes, rranks):
    nums, indexes, ranks = [], [], []
    i_left = i_right = 0

    def add_from_left():
        nonlocal i_left
        nums.append(lnums[i_left])
        indexes.append(lindexes[i_left])
        ranks.append(lranks[i_left])
        i_left += 1
    def add_from_right():
        nonlocal i_right
        nums.append(rnums[i_right])
        indexes.append(rindexes[i_right] + len(lnums))
        ranks.append(rranks[i_right] + i_left)
        i_right += 1

    while i_left < len(lnums) and i_right < len(rnums):
        if lnums[i_left] < rnums[i_right]:
            add_from_left()
        elif lnums[i_left] > rnums[i_right]:
            add_from_right()
        else:
            raise ValueError("Tie detected")

    if i_left < len(lnums):
        while i_left < len(lnums):
            add_from_left()
        #nums += lnums[i_left:]
        #indexes += lindexes[i_left:]
        #ranks += lranks[i_left:]
    else:
        while i_right < len(rnums):
            add_from_right()
    return nums, indexes, ranks

def rank_2D(f,nums): #f is method, nums is 2D numpy array
    result=[]
    for x in nums:
        result.append(f(x))
    return np.array(result)

x=np.random.rand(6000)
for f in [John, Matvei, Divarkar1, Divarkar2, Monica1, Monica2]:
    t1=time.time()
    rr=f(x)
    t2=time.time()
    print(f'{f.__name__+"_1D: ":16}  {(t2-t1):.3f}')
print()

x=np.random.rand(380,900)
t1=time.time()
rr=John_2D(x)
t2=time.time()
print(f'{"John_2D:":16}  {(t2-t1):.3f}')
#print(rr)

for f in [Matvei, Divarkar1, Divarkar2, Monica1, Monica2]:
    t1=time.time()
    rr=rank_2D(f,x)
    t2=time.time()
    print(f'{f.__name__+"_2D: ":16}  {(t2-t1):.3f}')
    #print(rr)

Типичные результаты:

John_1D:          0.069
Matvei_1D:        7.208
Divarkar1_1D:     0.163
Divarkar2_1D:     0.488
Monica1_1D:       0.032
Monica2_1D:       0.082

John_2D:          0.409
Matvei_2D:        49.044
Divarkar1_2D:     1.276
Divarkar2_2D:     4.065
Monica1_2D:       1.090
Monica2_2D:       3.571

Для 1D массива лучше всего использовать метод Monica1, но мой метод numpy -версии не так уж плох. Для двумерного массива лучше всего использовать мой numpy -версионный метод.

Добро пожаловать на тестирование и комментирование.

Спасибо

Джон

0 голосов
/ 30 апреля 2020

Вы весь мой герой! отлично справляюсь! Я хотел бы показать вам сравнение каждого из ваших решений:

import numpy as np
import time
import sortedcontainers

def John(x):
    n=len(x)
    rr=np.ones(n)
    for i in range(1,n):
        rr[i]=sum(x[i]>=x[:i+1])
    return rr

def Matvei(x):
    return [sorted(x[:i+1]).index(v)+1 for i, v in enumerate(x)]

def Divarkar1(x):
    n = len(x)
    m1 = x[1:,None]>=x
    m2 = np.tri(n-1,n,k=1, dtype=bool)
    rr[1:] = (m1 & m2).sum(1)
    return rr


def Divarkar2(x):
    n = len(x)
    m1 = x[1:,None]>=x
    m2 = np.tri(n-1,n,k=1, dtype=bool)
    (m1.astype(np.float32)[:,None,:] @ m2[:,:,None])[:,0,0]
    rr[1:]=np.einsum('ij,ij->i',m1.astype(np.float32),m2)
    return rr

def Monica(x):
    sortednums = sortedcontainers.SortedList()
    ranks = []
    for num in x:
        ranks.append(sortednums.bisect_left(num))
        sortednums.add(num)
    return np.array(ranks)+1


x=np.random.rand(4000)

t1=time.time()
rr=John(x)
t2=time.time()
print(t2-t1)
#print(rr)

t1=time.time()
rr=Matvei(x)
t2=time.time()
print(t2-t1)
#print(rr)

t1=time.time()
rr=Divarkar1(x)
t2=time.time()
print(t2-t1)
#print(rr)

t1=time.time()
rr=Divarkar2(x)
t2=time.time()
print(t2-t1)
#print(rr)

t1=time.time()
rr=Monica(x)
t2=time.time()
print(t2-t1)
#print(rr)

Результаты:

19,5

2,9

0,079

0,25

0,017

Я бегал несколько раз, и результаты аналогичные. Лучший из них - алгоритм Моники!

Большое спасибо всем!

Джон

...