Как я могу превратить вложенный цикл в идиоматический код numpy для создания матрицы результатов турниров?

#python #arrays #numpy

Вопрос:

У меня есть двумерный массив, где столбцы-турниры, а строки-результаты игроков. Что я хотел бы сделать, так это создать матрицу результатов, где каждая строка представляет частоту выигрышей конкретного игрока против всех остальных.

Вот мое наивное решение этой проблемы.

 import numpy as np

input_example = np.array([
    [55.90, 81.50, 76.60, 69.50],
    [52.50, 74.60, 74.00, 64.80],
    [52.40, 74.90, 78.20, 60.90],
    [52.60, 78.90, 77.60, 60.80],
    ])

output_example = np.array([
    [0.50, 1.00, 0.75, 0.75],
    [0.00, 0.50, 0.50, 0.25],
    [0.25, 0.50, 0.50, 0.50],
    [0.25, 0.75, 0.50, 0.50],
    ])

def results_matrix(tournament_results):
    players, tournaments = tournament_results.shape
    res_matrix = np.zeros(shape=(players, tournaments))
    for n in range(players):
        for m in range(players):
            if n == m:
                res_matrix[n][m] = 0.50
                continue
            res_matrix[n][m] = (tournament_results[n] > tournament_results[m]).sum() / tournaments
    return res_matrix

if __name__ == '__main__':
    res_matrix = results_matrix(input_example)
    assert np.array_equal(res_matrix, output_example)
 

Хотя это дает правильные результаты, я хотел бы превратить это в идиоматическое решение numpy, чтобы оно быстро работало с большими входными данными.

Вы можете предположить, что результаты в конкретном турнире различны, так что ни одна пара игроков не может иметь идентичного результата.

Ответ №1:

Как насчет этого решения?

 players, tournaments = input_example.shape
arr0 = input_example.reshape(players, 1, tournaments)
arr1 = input_example.reshape(1, players, tournaments)
res_matrix = (arr0 > arr1).sum(2)/tournaments
res_matrix[np.arange(players), np.arange(players)] = 0.5
 

Редактировать:
Если у вас недостаточно памяти, чтобы сделать это таким образом, то не будет никаких обходных циклов. Но вы можете просто использовать numba, чтобы ваш код работал быстрее, как сейчас.

 import numpy as np
from numba import jit

input_example = np.array([
    [55.90, 81.50, 76.60, 69.50],
    [52.50, 74.60, 74.00, 64.80],
    [52.40, 74.90, 78.20, 60.90],
    [52.60, 78.90, 77.60, 60.80],
    ])

output_example = np.array([
    [0.50, 1.00, 0.75, 0.75],
    [0.00, 0.50, 0.50, 0.25],
    [0.25, 0.50, 0.50, 0.50],
    [0.25, 0.75, 0.50, 0.50],
    ])

def results_matrix(tournament_results):
    players, tournaments = tournament_results.shape
    res_matrix = np.zeros(shape=(players, players))
    for n in range(players):
        for m in range(players):
            if n == m:
                res_matrix[n][m] = 0.50
                continue
            res_matrix[n][m] = (tournament_results[n] > tournament_results[m]).sum() / tournaments
    return res_matrix

@jit(nopython=True)
def results_matrix_ba(tournament_results):
    players, tournaments = tournament_results.shape
    res_matrix = np.zeros(shape=(players, players))
    for n in range(players):
        for m in range(players):
            if n == m:
                res_matrix[n][m] = 0.50
                continue
            res_matrix[n][m] = (tournament_results[n] > tournament_results[m]).sum() / tournaments
    return res_matrix

print('running w/o numba')
%timeit assert np.array_equal(output_example, results_matrix(input_example))
print('running with numba')
%timeit assert np.array_equal(output_example, results_matrix_ba(input_example))
 

Выход:

 running w/o numba
The slowest run took 4.10 times longer than the fastest. This could mean that an intermediate result is being cached.
10000 loops, best of 5: 71.9 µs per loop
running with numba
The slowest run took 5.06 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 5: 7.74 µs per loop
 

Еще один пример:

 np.random.seed(0)
input_example = np.random.rand(900, 500)

print('running w/o numba')
%timeit results_matrix(input_example)
print('running with numba')
%timeit results_matrix_ba(input_example)
 

Выход:

 running w/o numba
1 loop, best of 5: 4.72 s per loop
running with numba
1 loop, best of 5: 407 ms per loop
 

Комментарии:

1. При попытке использовать его на большем входном размере 9000×5000 я получил MemoryError: Unable to allocate 377. GiB for an array with shape (9000, 9000, 5000) and data type bool

2. @KonstantinKostanzhoglo Я добавил еще один вариант, который, я думаю, может помочь в вашем случае

3. отличная работа, с numba он работает в 4 раза быстрее для массива размером 9000×5000. Мне интересно, можно ли добавить многопроцессорную обработку для достижения дальнейшего ускорения?

4. обновление: Я использовал @njit(parallel=True) декоратор с prange вместо range , и это дало 7-кратное ускорение по @jit(nopython=True) сравнению с декоратором и 28-кратное ускорение по сравнению с моим первоначальным решением для массива размером 9000×5000.

5. @KonstantinKostanzhoglo хорошая работа, спасибо за обновление.