Условная кумулятивная кумулятивная сумма

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

Например

a = np.asarray([0, 4999, -5000, 1000])
np.cumsum(a)

возвращается [0, 4999, -1, 999]

но я бы хотел установить [2]-значение (-1) до нуля во время расчета. Проблема в том, что это решение может быть принято только во время расчета, поскольку промежуточный результат не известен априори.

Ожидаемый массив: [0, 4999, 0, 1000]

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

3 ответа

Алгоритм суммирования Кахана может решить эту проблему. К сожалению, это не реализовано в NumPy. Это означает, что требуется индивидуальная реализация:

def kahan_cumsum(x):
    x = np.asarray(x)
    cumulator = np.zeros_like(x)
    compensation = 0.0

    cumulator[0] = x[0]    
    for i in range(1, len(x)):
        y = x[i] - compensation
        t = cumulator[i - 1] + y
        compensation = (t - cumulator[i - 1]) - y
        cumulator[i] = t
    return cumulator

Я должен признать, что это не совсем то, о чем просили в этом вопросе. (Значение -1 на 3-м выходе cumsum является правильным в примере). Однако я надеюсь, что это решает реальную проблему, стоящую за этим вопросом, которая связана с точностью с плавающей запятой.

Вероятно, лучший способ - написать этот бит на Cython (назовите файл cumsum_eps.pyx):

cimport numpy as cnp
import numpy as np

cdef inline _cumsum_eps_f4(float *A, int ndim, int dims[], float *out, float eps):
    cdef float sum
    cdef size_t ofs

    N = 1
    for i in xrange(0, ndim - 1):
        N *= dims[i]
    ofs = 0
    for i in xrange(0, N):
        sum = 0
        for k in xrange(0, dims[ndim-1]):
            sum += A[ofs]
            if abs(sum) < eps:
                sum = 0
            out[ofs] = sum
            ofs += 1

def cumsum_eps_f4(cnp.ndarray[cnp.float32_t, mode='c'] A, shape, float eps):
    cdef cnp.ndarray[cnp.float32_t] _out
    cdef cnp.ndarray[cnp.int_t] _shape
    N = np.prod(shape)
    out = np.zeros(N, dtype=np.float32)
    _out = <cnp.ndarray[cnp.float32_t]> out
    _shape = <cnp.ndarray[cnp.int_t]> np.array(shape, dtype=np.int)
    _cumsum_eps_f4(&A[0], len(shape), <int*> &_shape[0], &_out[0], eps)
    return out.reshape(shape)


def cumsum_eps(A, axis=None, eps=np.finfo('float').eps):
    A = np.array(A)
    if axis is None:
        A = np.ravel(A)
    else:
        axes = list(xrange(len(A.shape)))
        axes[axis], axes[-1] = axes[-1], axes[axis]
        A = np.transpose(A, axes)
    if A.dtype == np.float32:
        out = cumsum_eps_f4(np.ravel(np.ascontiguousarray(A)), A.shape, eps)
    else:
        raise ValueError('Unsupported dtype')
    if axis is not None: out = np.transpose(out, axes)
    return out

затем вы можете скомпилировать его так (Windows, Visual C++ 2008 Command Line):

\Python27\Scripts\cython.exe cumsum_eps.pyx
cl /c cumsum_eps.c /IC:\Python27\include /IC:\Python27\Lib\site-packages\numpy\core\include
F:\Users\sadaszew\Downloads>link /dll cumsum_eps.obj C:\Python27\libs\python27.lib /OUT:cumsum_eps.pyd

или как это (Linux использовать.so расширение /Cygwin использовать.dll расширение, GCC):

cython cumsum_eps.pyx
gcc -c cumsum_eps.c -o cumsum_eps.o -I/usr/include/python2.7 -I/usr/lib/python2.7/site-packages/numpy/core/include
gcc -shared cumsum_eps.o -o cumsum_eps.so -lpython2.7

и используйте так:

from cumsum_eps import *
import numpy as np
x = np.array([[1,2,3,4], [5,6,7,8]], dtype=np.float32)

>>> print cumsum_eps(x)
[  1.   3.   6.  10.  15.  21.  28.  36.]
>>> print cumsum_eps(x, axis=0)
[[  1.   2.   3.   4.]
 [  6.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=1)
[[  1.   3.   6.  10.]
 [  5.  11.  18.  26.]]
>>> print cumsum_eps(x, axis=0, eps=1)
[[  1.   2.   3.   4.]
 [  6.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=2)
[[  0.   2.   3.   4.]
 [  5.   8.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=3)
[[  0.   0.   3.   4.]
 [  5.   6.  10.  12.]]
>>> print cumsum_eps(x, axis=0, eps=4)
[[  0.   0.   0.   4.]
 [  5.   6.   7.  12.]]
>>> print cumsum_eps(x, axis=0, eps=8)
[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  8.]]
>>> print cumsum_eps(x, axis=1, eps=3)
[[  0.   0.   3.   7.]
 [  5.  11.  18.  26.]]

и так далее, конечно, обычно eps будет небольшим значением, здесь целые числа используются только для демонстрации / простоты ввода.

Если вам нужно это также и для double, варианты _f8 тривиальны для записи, и другой случай должен быть обработан в cumsum_eps().

Когда вы довольны реализацией, вы должны сделать ее правильной частью вашего setup.py - Cython setup.py

Обновление № 1: Если у вас есть хорошая поддержка компилятора в среде выполнения, вы можете попробовать [Theano][3] реализовать алгоритм компенсации или вашу оригинальную идею:

import numpy as np
import theano
import theano.tensor as T
from theano.ifelse import ifelse

A=T.vector('A')

sum=T.as_tensor_variable(np.asarray(0, dtype=np.float64))

res, upd=theano.scan(fn=lambda cur_sum, val: ifelse(T.lt(cur_sum+val, 1.0), np.asarray(0, dtype=np.float64), cur_sum+val), outputs_info=sum, sequences=A)

f=theano.function(inputs=[A], outputs=res)

f([0.9, 2, 3, 4])

выдаст [0 2 3 4] вывод. В Cython или в этом вы получаете как минимум +/- производительность нативного кода.

Интересно, будет ли округление делать то, что вы просите:

np.cumsum(np.around(a,-1))
# the -1 means it rounds to the nearest 10

дает

array([   0, 5000,    0, 1000])

Это не совсем то, что вы положили в ожидаемый массив из вашего ответа, но с помощью around возможно с decimals параметр, установленный в 0, может работать, когда вы применяете его к проблеме с плавающей точкой.

Другие вопросы по тегам