具有上限/下限的Numpy自定义Cumsum函数?

时间:2018-11-01 15:50:54

标签: python algorithm pandas performance numpy

我有一个numpy / pandas值列表:

a = np.random.randint(-100, 100, 10000)
b = a/100

我想应用自定义的cumsum函数,但是我还没有找到一种没有循环的方法。自定义函数将累加值设置为1的上限和-1的下限,如果求和的“加”超出了这些限制,则“加”变为0。

如果总和在-1和1的限制之间,但“加”的值将超出限制,则“加”将成为-1或1的余数。

这是循环版本:

def cumsum_with_limits(values):
    cumsum_values = []
    sum = 0
    for i in values:
        if sum+i <= 1 and sum+i >= -1: 
            sum += i
            cumsum_values.append(sum)
        elif sum+i >= 1:
            d = 1-sum # Remainder to 1
            sum += d
            cumsum_values.append(sum)
        elif sum+i <= -1:
            d = -1-sum # Remainder to -1
            sum += d
            cumsum_values.append(sum)

    return cumsum_values

有什么方法可以对此向量化?我需要在大型数据集上运行此功能,而性能是我当前的问题。感谢任何帮助!


更新:修复了一些代码,并对输出进行了一些澄清: 使用np.random.seed(0),前6个值是:

b = [0.72, -0.53, 0.17, 0.92, -0.33, 0.95]

预期输出:

o = [0.72, 0.19, 0.36, 1, 0.67, 1]

2 个答案:

答案 0 :(得分:4)

循环不一定是不希望的。如果性能是一个问题,请考虑使用numba。在不实质改变逻辑的情况下,性能提高了约330倍:

from numba import njit

np.random.seed(0)
a = np.random.randint(-100, 100, 10000)
b = a/100

@njit
def cumsum_with_limits_nb(values):
    n = len(values)
    res = np.empty(n)
    sum_val = 0
    for i in range(n):
        x = values[i]
        if (sum_val+x <= 1) and (sum_val+x >= -1):
            res[i] = x
            sum_val += x
        elif sum_val+x >= 1:
            d = 1-sum_val # Remainder to 1
            res[i] = d
            sum_val += d
        elif sum_val+x <= -1:
            d = -1-sum_val # Remainder to -1
            res[i] = d
            sum_val += d
    return res

assert np.isclose(cumsum_with_limits(b), cumsum_with_limits_nb(b)).all()

如果您不介意牺牲某些性能,则可以更简洁地重写此循环:

@njit
def cumsum_with_limits_nb2(values):
    n = len(values)
    res = np.empty(n)
    sum_val = 0
    for i in range(n):
        x = values[i]
        next_sum = sum_val + x
        if np.abs(next_sum) >= 1:
            x = np.sign(next_sum) - sum_val
        res[i] = x
        sum_val += x
    return res

nb2具有类似的性能,这是一种替代方法(感谢@jdehesa):

@njit
def cumsum_with_limits_nb3(values):
    n = len(values)
    res = np.empty(n)
    sum_val = 0
    for i in range(n):
        x = min(max(sum_val + values[i], -1) , 1) - sum_val
        res[i] = x
        sum_val += x
    return res

性能比较:

assert np.isclose(cumsum_with_limits(b), cumsum_with_limits_nb(b)).all()
assert np.isclose(cumsum_with_limits(b), cumsum_with_limits_nb2(b)).all()
assert np.isclose(cumsum_with_limits(b), cumsum_with_limits_nb3(b)).all()

%timeit cumsum_with_limits(b)      # 12.5 ms per loop
%timeit cumsum_with_limits_nb(b)   # 40.9 µs per loop
%timeit cumsum_with_limits_nb2(b)  # 54.7 µs per loop
%timeit cumsum_with_limits_nb3(b)  # 54 µs per loop

答案 1 :(得分:2)

以常规的累积金额开头:

b = ...
s = np.cumsum(b)

找到第一个剪辑点:

i = np.argmax((s[0:] > 1) | (s[0:] < -1))

调整以下内容:

s[i:] += (np.sign(s[i]) - s[i])

冲洗并重复。这仍然需要一个循环,但仅在调整点上进行,通常希望该调整点比数组大小的总数小得多。

b = ...
s = np.cumsum(b)
while True:
    i = np.argmax((s[0:] > 1) | (s[0:] < -1))
    if np.abs(s[i]) <= 1:
        break
    s[i:] += (np.sign(s[i]) - s[i])

我仍然没有找到一种方法可以完全预先预先计算调整点,因此,即使要使用numba进行编译,我也不得不猜测numba解决方案会比此更快。

np.seed(0)开始,您的原始示例具有3090个调整点,大约为1/3。不幸的是,使用所有临时数组和额外的和,这使我的解决方案的算法复杂度趋于O(n 2 )。这是完全不能接受的。