Кумулятивное сложение / умножение в NumPy
есть относительно простой блок кода, который проходит через два массива, умножается и добавляет кумулятивно:
import numpy as np
a = np.array([1, 2, 4, 6, 7, 8, 9, 11])
b = np.array([0.01, 0.2, 0.03, 0.1, 0.1, 0.6, 0.5, 0.9])
c = []
d = 0
for i, val in enumerate(a):
d += val
c.append(d)
d *= b[i]
есть ли способ сделать это без перебора? Я предполагаю, что cumsum/cumprod можно использовать, но у меня возникли проблемы с выяснением того, как. Когда вы сломать то, что происходит, шаг за шагом это выглядит так:
# 0: 0 + a[0]
# 1: ((0 + a[0]) * b[0]) + a[1]
# 2: ((((0 + a[0]) * b[0]) + a[1]) * b[1]) + a[2]
Edit для уточнения: меня интересует список (или массив) c.
4 ответов
в каждой итерации у вас есть -
d[n+1] = d[n] + a[n]
d[n+1] = d[n+1] * b[n]
таким образом, по существу -
d[n+1] = (d[n] + a[n]) * b[n]
то есть -
d[n+1] = (d[n]* b[n]) + K[n] #where `K[n] = a[n] * b[n]`
теперь, используя эту формулу, Если вы запишите выражения для до n = 2
случаи, вы -
d[1] = d[0]*b[0] + K[0]
d[2] = d[0]*b[0]*b[1] + K[0]*b[1] + K[1]
d[3] = d[0] * b[0]*b[1] * b[2] + K[0]*b[1]*b[2] + K[1]*b[2] + K[2]
Scalars : b[0]*b[1]*b[2] b[1]*b[2] b[2] 1
Coefficients : d[0] K[0] K[1] K[2]
таким образом, вам понадобится обратный cumprod b
, выполните элементарное умножение с K
массив. Наконец, чтобы получить c
выполнить cumsum
и с c
сохраняется перед масштабированием на b
, поэтому вам нужно будет уменьшить cumsum
версия обращенным cumprod b
.
окончательная реализация будет выглядеть так -
# Get reversed cumprod of b and pad with `1` at the end
b_rev_cumprod = b[::-1].cumprod()[::-1]
B = np.hstack((b_rev_cumprod,1))
# Get K
K = a*b
# Append with 0 at the start, corresponding starting d
K_ext = np.hstack((0,K))
# Perform elementwsie multiplication and cumsum and scale down for final c
sums = (B*K_ext).cumsum()
c = sums[1:]/b_rev_cumprod
тесты во время выполнения и проверка вывод
функции определения -
def original_approach(a,b):
c = []
d = 0
for i, val in enumerate(a):
d = d+val
c.append(d)
d = d*b[i]
return c
def vectorized_approach(a,b):
b_rev_cumprod = b[::-1].cumprod()[::-1]
B = np.hstack((b_rev_cumprod,1))
K = a*b
K_ext = np.hstack((0,K))
sums = (B*K_ext).cumsum()
return sums[1:]/b_rev_cumprod
время выполнения и проверка
Случай #1: случай образца OP
In [301]: # Inputs
...: a = np.array([1, 2, 4, 6, 7, 8, 9, 11])
...: b = np.array([0.01, 0.2, 0.03, 0.1, 0.1, 0.6, 0.5, 0.9])
...:
In [302]: original_approach(a,b)
Out[302]:
[1,
2.0099999999999998,
4.4020000000000001,
6.1320600000000001,
7.6132059999999999,
8.7613205999999995,
14.256792359999999,
18.128396179999999]
In [303]: vectorized_approach(a,b)
Out[303]:
array([ 1. , 2.01 , 4.402 , 6.13206 ,
7.613206 , 8.7613206 , 14.25679236, 18.12839618])
Case #2: большой входной корпус
In [304]: # Inputs
...: N = 1000
...: a = np.random.randint(0,100000,N)
...: b = np.random.rand(N)+0.1
...:
In [305]: np.allclose(original_approach(a,b),vectorized_approach(a,b))
Out[305]: True
In [306]: %timeit original_approach(a,b)
1000 loops, best of 3: 746 µs per loop
In [307]: %timeit vectorized_approach(a,b)
10000 loops, best of 3: 76.9 µs per loop
пожалуйста, помните, что для чрезвычайно огромных случаев входного массива, если b
элементы такие малые доли, из-за кумулятивных операций, начальные числа b_rev_cumprod
может выйти как zeros
в результате NaNs
в этих начальные места.
давайте посмотрим, если мы можем сделать еще быстрее. Теперь я покидаю Чистый мир python и показываю, что эти чисто числовые проблемы могут быть оптимизированы еще больше.
два игрока являются быстрой векторизованной версией @ Divakar:
def vectorized_approach(a,b):
b_rev_cumprod = b[::-1].cumprod()[::-1]
B = np.hstack((b_rev_cumprod,1))
K = a*b
K_ext = np.hstack((0,K))
sums = (B*K_ext).cumsum()
return sums[1:]/b_rev_cumprod
и версия cython:
%%cython
import numpy as np
def cython_approach(long[:] a, double[:] b):
cdef double d
cdef size_t i, n
n = a.shape[0]
cdef double[:] c = np.empty(n)
d = 0
for i in range(n):
d += a[i]
c[i] = d
d *= b[i]
return c
версия cython примерно в 5 раз быстрее, чем векторизованная версия:
%timeit vectorized_approach(a,b)
->10000 loops, best of 3: 43.4 µs per loop
%timeit cython_approach(a,b)
->100000 loops, best of 3: 7.7 µs per loop
еще один плюс цитона версия заключается в том, что она гораздо более читабельна.
большой недостаток заключается в том, что вы оставляете чистый python, и в зависимости от вашего варианта использования компиляция модуля расширения может не быть вариантом для вас.
это здесь работает для меня и векторизовано
b_mat = np.tile(b,(b.size,1)).T
b_mat = np.vstack((np.ones(b.size),b_mat))
np.fill_diagonal(b_mat,1)
b_mat[np.triu_indices(b.size)]=1
b_prod_mat = np.cumprod(b_mat,axis=0)
b_prod_mat[np.triu_indices(b.size)] = 0
np.fill_diagonal(b_prod_mat,1)
c = np.dot(b_prod_mat,a)
c
# output
array([ 1. , 2.01 , 4.402, 6.132, 7.613, 8.761, 14.257,
18.128, 16.316])
Я согласен, что нелегко понять, что происходит. Ваш массив c
можно записать как умножение матрицы-вектора b_prod_mat * a
здесь a
- это Ваш массив и b_prod_mat
состоит из конкретных продуктов b
. Все внимание в основном, чтобы создать b_prod_mat
.
Я не уверен, что это лучше, чем цикл for, но вот так:
a.dot([np.concatenate((np.zeros(i), (1, ), b[i:-1])) for i in range(len(b))])
что он делает, это создает строку большой матрицы A
такой:
1 b0 b0b1 b0b1b2 ... b0b1..bn-1
0 1 b1 b1b2 ... b1..bn-1
0 0 1 b2 ...
...
0 0 0 0 ... 1
тогда вы просто умножаете вектор a
с матрицей A
и вы получаете ожидаемый результат.