Кумулятивное сложение / умножение в 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 и вы получаете ожидаемый результат.