Эффективная накопленная сумма в Python - PullRequest
2 голосов
/ 12 марта 2020

У меня есть вектор a известного размера N, так что np.sum(a) равно 1, а np.all(a>=0) соответствует истине. Я хочу определить минимальное количество записей, которые суммируют порог t. Например, я бы сделал что-то вроде:

idx = np.argsort(a)
asorted = a[idx][::-1]
sum_ = 0
number = 0
while sum_ < t:
    number += 1
    sum_ = np.sum(asorted[:number])

, как только sum_ превысит t, программа остановится, а переменная number сообщит мне минимальное количество записей, которые суммируются этот порог.

Я ищу наиболее эффективный способ получения этого числа, так как я должен выполнить эту операцию миллионы раз.

Ответы [ 4 ]

3 голосов
/ 12 марта 2020

( EDITED )

( EDIT2 : добавлена ​​более специализированная версия JIT для решения проблем при использовании np.sort() с numba.)

( EDIT3 : включено время для рекурсивного подхода с медианным поворотом от @ hilberts_drinking_problem's answer )

Я не 100%, что вы после, потому что первые две строки вашего кода, кажется, ничего не делают, но после @hilberts_drinking_problem я отредактировал свой ответ, я предполагаю, что у вас есть опечатка и:

sum_ = np.sum(arr[:i])

должно быть:

sum_ = np.sum(asorted[:i])

Тогда ваше решение можно записать в виде функции, такой как:

import numpy as np


def min_sum_threshold_orig(arr, threshold=0.5):
    idx = np.argsort(arr)
    arr_sorted = arr[idx][::-1]
    sum_ = 0
    i = 0
    while sum_ < threshold:
        i += 1
        sum_ = np.sum(arr_sorted[:i])
    return i

Однако:

  1. Вместо np.argsort() и индексирование, которое вы можете использовать np.sort() напрямую
  2. нет необходимости вычислять всю сумму на каждой итерации, но вместо этого вы можете использовать сумму из предыдущей итерации
  3. Использование while l oop рискованно, потому что если threshold достаточно высоко (> 1.0 с вашим предположением), тогда l oop ever end

Обращаясь к этим точкам, можно получить:

def min_sum_threshold(arr, threshold=0.5):
    arr = np.sort(arr)[::-1]
    sum_ = 0
    for i in range(arr.size):
        sum_ += arr[i]
        if sum_ >= threshold:
            break
    return i + 1

В приведенном выше описании явное зацикливание становится узким местом. Хороший способ решения этой проблемы - использовать numba:

import numba as nb


min_sum_threshold_nbn = nb.jit(min_sum_threshold)
min_sum_threshold_nbn.__name__ = 'min_sum_threshold_nbn'

Но это может быть не самый эффективный подход, поскольку numba является относительно медленным при создании новых массивов. Возможно, более быстрый подход заключается в использовании arr.sort() вместо np.sort(), потому что это на месте, что позволяет избежать создания нового массива:

@nb.jit
def min_sum_thres_nb_inplace(arr, threshold=0.5):
    arr.sort()
    sum_ = 0
    for i in range(arr.size - 1, -1, -1):
        sum_ += arr[i]
        if sum_ >= threshold:
            break
    return arr.size - i

В качестве альтернативы можно выполнить JIT только часть кода после сортировки:

@nb.jit
def _min_sum_thres_nb(arr, threshold=0.5):
    sum_ = 0.0
    for i in range(arr.size):
        sum_ += arr[i]
        if sum_ >= threshold:
            break
    return i + 1


def min_sum_thres_nb(arr, threshold=0.5):
    return _min_sum_thres_nb(np.sort(arr)[::-1], threshold)

Разница между ними будет минимальной для больших входов. Для меньшего из них min_sum_thres_nb() будет зависеть от сравнительно медленного вызова дополнительной функции. Из-за ошибок в функциях бенчмаркинга, которые изменяют их входные данные, min_sum_thres_nb_inplace() опускается в бенчмарках с пониманием того, что для очень маленьких входов он такой же быстрый, как min_sum_thres_nbn(), а для более крупных он имеет практически те же характеристики, что и min_sum_thres_nb().


В качестве альтернативы можно использовать векторизованные подходы, как в @ yatu's answer :

def min_sum_threshold_np_sum(arr, threshold=0.5):
    return np.sum(np.cumsum(np.sort(arr)[::-1]) < threshold) + 1

или, лучше, использовать np.searchsorted(), что позволяет избежать создания ненужных временный массив со сравнением:

def min_sum_threshold_np_ss(arr, threshold=0.5):
    return np.searchsorted(np.cumsum(np.sort(arr)[::-1]), threshold) + 1

или, если предположить, что сортировка всего массива излишне дорогая:

def min_sum_threshold_np_part(arr, threshold=0.5):
    n = arr.size
    m = np.int(size * threshold) + 1
    part_arr = np.partition(arr, n - m)[n - m:]
    return np.searchsorted(np.cumsum(np.sort(arr)[::-1]), threshold) + 1

Еще более сложный подход с использованием рекурсии и медианного поворота:

def min_sum_thres_rec(arr, threshold=0.5, cutoff=64):
    n = arr.size
    if n <= cutoff:
        return np.searchsorted(np.cumsum(np.sort(arr)[::-1]), threshold) + 1
    else:
        m = n // 2
        partitioned = np.partition(arr, m)
        low = partitioned[:m]
        high = partitioned[m:]
        sum_high = np.sum(high)
        if sum_high >= threshold:
            return min_sum_thres_rec(high, threshold)
        else:
            return min_sum_thres_rec(low, threshold - sum_high) + high.size

(последние три адаптированы из ответа @ hilberts_drinking_problem )


Сравнительный анализ с входными данными, сгенерированными из этого:

def gen_input(n, a=0, b=10000):
    arr = np.random.randint(a, b, n)
    arr = arr / np.sum(arr)
    return arr

дает следующее:

bm_full bm_zoom

Они указывают на то, что для достаточно малых входов, утверждение numba Aч - самый быстрый, но как только вход превышает ~ 600 элементов для наивного подхода или ~ 900 для оптимизированного , подход NumPy, который использует np.partition(), в то же время менее эффективно использует память, быстрее.

В конечном итоге, после ~ 4000 элементов, min_sum_thres_rec() становится быстрее, чем все другие предложенные методы. Может быть возможно написать более быструю реализацию этого метода на основе чисел.

Обратите внимание, что оптимизированный numba подход строго быстрее, чем наивные NumPy протестированные подходы.

3 голосов
/ 12 марта 2020

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

def min_sum_rec(arr, t=0.5):
  n = arr.size
  # default to sorting for small arrays
  if n <= 100:
    return np.searchsorted(np.sort(arr)[::-1].cumsum(), t) + 1
  partitioned = np.partition(arr, n//2)
  low = partitioned[:n//2]
  high = partitioned[n//2:]
  sum_high = high.sum()
  if sum_high >= t:
    return min_sum_rec(high, t)
  else:
    return min_sum_rec(low, t - sum_high) + high.size

Вот сравнение с моим предыдущим решением O (n log (n)), в секундах :

N           min_sum_rec  num_to_t
10            0.000041  0.000038
100           0.000025  0.000028
1000          0.000086  0.000042
10000         0.000321  0.000310
100000        0.002547  0.003259
1000000       0.028826  0.039854
10000000      0.247731  0.431744
100000000     2.371766  4.800107

Предыдущее решение, которое может быть быстрее для массивов меньшего размера:

Помимо использования cumsum, обратите внимание, что средний элемент массива имеет размер 1/N. Следовательно, требуется не более t*N элементов для суммирования до t. Для небольших t это дает возможность оптимизации, когда мы делаем O(N) вызов np.partition с последующей сортировкой t*N самых больших элементов:

import numpy as np
np.random.seed(0)

a = np.random.rand(10**6)
a /= a.sum()
t = 1e-3


def num_to_t_sort(a, t):
  c = np.sort(a)[::-1].cumsum()
  return np.searchsorted(c, t) + 1


def num_to_t(a, t):
  n = len(a)
  m = np.int(n * t) + 1
  b = np.partition(a, n-m)[n-m:]
  b[::-1].sort()
  c = b.cumsum()
  return np.searchsorted(c, t) + 1


assert num_to_t(a, t) == num_to_t_sort(a, t)
%timeit num_to_t(a, t)      # 100 loops, best of 3: 11.8 ms per loop
%timeit num_to_t_sort(a, t) # 10 loops, best of 3: 107 ms per loop

Аналогичная оптимизация применяется, если t имеет тенденцию быть близко к 1. Если вы повторяете операции для одного и того же массива и разных t, вам, вероятно, лучше хранить c = np.sort(a)[::-1].cumsum() и вызывать np.searchsorted для каждого t.

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

1 голос
/ 12 марта 2020

Вот подход на основе NumPy:

(np.cumsum(np.sort(a)[::-1]) < t).sum() + 1

Например:

a = np.array([1,2,8,5,13,9])

(np.cumsum(np.sort(a)[::-1]) < 25).sum() + 1
# 3

Где:

np.sort(a)[::-1]
# array([13,  9,  8,  5,  2,  1])

И:

np.cumsum(np.sort(a)[::-1])
# array([13, 22, 30, 35, 37, 38])
1 голос
/ 12 марта 2020

В зависимости от длины массивов, возможно сортировать, и тогда вы могли бы использовать совокупную сумму ? По крайней мере, это будет быстрее, чем это, в то время как l oop.

>>> a = np.array(range(10)[::-1])
>>> a
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
>>> b = np.cumsum(a)
>>> b
array([ 9, 17, 24, 30, 35, 39, 42, 44, 45, 45])

Затем просто используйте argmax, скажем, вы хотите индекс, где он прошел 40:

>>> np.argmax(b > 40)
6
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...