如何对 numpy 数组的 n 个元素求和

How to sum across n elements of numpy array

我希望有人能帮助我解决我的问题,因为我还不习惯 python 和 numpy。我有以下包含 24 个元素的数组:

load = np.array([10, 12, 9, 13, 17, 23, 25, 28, 26, 24, 22, 20, 18, 20, 22, 24, 26, 28, 23, 24, 21, 18, 16, 13])

我想创建一个与“load”长度相同的新数组,并为数组中的每个元素计算当前和下两个数字的总和,这样我的 objective 数组看起来像这样:

[31, 34, 39, 53, 65, 76, 79, 78, 72, 66, 60, 58, 60, 66, 72, 78, 77, 75, 68, 63, 55, 47, 29, 13]

我尝试使用以下代码解决此问题:

output = np.empty(len(load))
for i in range((len(output))-2):
    output[i] = load[i]+load[i+1]+load[i+2]
print(output)

输出数组如下所示:

array([31. , 34. , 39. , 53. , 65. , 76. , 79. , 78. , 72. , 66. , 60. ,
       58. , 60. , 66. , 72. , 78. , 77. , 75. , 68. , 63. , 55. , 47. ,
        6. ,  4.5])

最后两个数字不对。对于第 23 个元素,我只想要 16 和 13 的总和,最后一个数字保持 13,因为数组在那里结束。我不明白 python 是如何计算这些数字的。我也希望数字是没有点的整数。

有没有人想到更好的解决方案?我知道这可能很容易解决,我只是不知道 numpy 的所有功能。

非常感谢!

如果数组不是超长,并且您不太关心内存利用率,您可以使用:

from itertools import zip_longest

output = [sum([x, y, z]) for x, y, z in zip_longest(load, load[1:], load[2:], fillvalue=0)]

输出为:

[31, 34, 39, 53, 65, 76, 79, 78, 72, 66, 60, 58, 60, 66, 72, 78, 77, 75, 68, 63, 55, 47, 29, 13]

np.empty 创建一个包含未初始化数据的数组。在您的代码中,您初始化了一个长度为 24 的数组 output,但只为其分配了 22 个值。最后 2 个值包含任意值(即垃圾)。除非性能很重要,否则 np.zeros 通常是初始化数组的更好选择,因为所有值都将具有一致的值 0。

您可以在不使用 for 循环的情况下解决此问题,方法是用零填充输入数组,然后计算矢量化和。

import numpy as np

load = np.array([10, 12, 9, 13, 17, 23, 25, 28, 26, 24, 22, 20, 18, 20, 22, 24, 26, 28, 23, 24, 21, 18, 16, 13])
tmp = np.pad(load, [0, 2])
output = load + tmp[1:-1] + tmp[2:]

print(output)

输出

[31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]

我将在您的来源中解决“Python 如何计算这两个数字”的问题:它们不是由您的程序计算的。 如果您注意到,您的主循环一直运行到数组的末尾,但最后两个元素。那些的价值没有被触及。由于这个原因,它对应于内存中与 np.empty() 分配的内存相对应的位置的数据。事实上,np.empty()只会获取内存的所有权,而不会初始化(即不改变其内容)。


一种简单的方法是遍历并对原始数组的不同视图求和:

def sum_next_k_loop(arr, k):
    result = arr.copy()
    for i in range(1, k):
        result[:-i] += arr[i:]
    return result

对于相对较小的 k 值来说,这是相当快的,但是随着 k 变大,可能需要避免相对较慢的显式循环。 一种方法是使用 strides 创建一个数组视图,该视图可用于沿额外维度求和。 这种方法在输入末尾留下了部分和。

可以从 zero-padded 输入开始:

import numpy as np
import numpy.lib.stride_tricks


def sum_next_k_strides(arr, k):
    n = arr.size
    result = np.zeros(arr.size + k - 1, dtype=arr.dtype)
    result[:n] = arr
    window = (k,) * result.ndim
    window_size = k ** result.ndim
    reduced_shape = tuple(dim - k + 1 for dim, k in zip(result.shape, window))
    view = np.lib.stride_tricks.as_strided(
        result, shape=reduced_shape + window, strides=arr.strides * 2, writeable=False)
    result = np.sum(view, axis=-1)
    return result

或者,为了提高内存效率,之后用 np.cumsum():

构建尾部
import numpy as np
import numpy.lib.stride_tricks


def sum_next_k_strides_cs(arr, k):
    n = arr.size
    window = (k,) * arr.ndim
    window_size = k ** arr.ndim
    reduced_shape = tuple(dim - k + 1 for dim, k in zip(arr.shape, window))
    view = np.lib.stride_tricks.as_strided(
        arr, shape=reduced_shape + window, strides=arr.strides * 2, writeable=False)
    result = np.empty_like(arr)
    result[:n - k + 1] = np.sum(view, axis=-1)
    result[n - k:] = np.cumsum(arr[-1:-(k + 1):-1])[::-1]
    return result

请注意,无论输入如何,循环遍历输入大小而不是 k 都不会很快,因为 k 受输入大小的限制。

另一种方法是,可以使用 np.convolve(),它可以精确地计算出你想要的东西,但是有两条尾巴,所以你只需要切掉开始的尾巴:

def sum_next_k_conv(arr, k):
    return np.convolve(arr, (1,) * k)[(k - 1):]

最后,可以编写一个使用 Numba 加速的完全显式循环解决方案:

import numpy as np
import numba as nb


@nb.njit
def running_sum_nb(arr, k):
    n = arr.size
    m = n - k + 1
    o = k - 1
    result = np.zeros(n, dtype=arr.dtype)
    # : fill bulk
    for j in range(m):
        tot = arr[j]
        for i in range(1, k):
            tot += arr[j + i]
        result[0 + j] = tot
    # : fill tail
    for j in range(o):
        tot = 0
        for i in range(j, o):
            tot += arr[m + i]
        result[m + j] = tot
    return result

检查所有解决方案是否给出与预期输出相同的结果:

funcs = running_sum_loop, running_sum_strides, running_sum_strides_cs, running_sum_conv, running_sum_nb

load = np.array([10, 12, 9, 13, 17, 23, 25, 28, 26, 24, 22, 20, 18, 20, 22, 24, 26, 28, 23, 24, 21, 18, 16, 13])
tgt = np.array([31, 34, 39, 53, 65, 76, 79, 78, 72, 66, 60, 58, 60, 66, 72, 78, 77, 75, 68, 63, 55, 47, 29, 13])

print(f"{'Input':>24}  {load}")
print(f"{'Target':>24}  {tgt}")
for i, func in enumerate(funcs, 1):
    print(f"{func.__name__:>24}  {func(load, 3)}")
                   Input  [10 12  9 13 17 23 25 28 26 24 22 20 18 20 22 24 26 28 23 24 21 18 16 13]
                  Target  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]
        running_sum_loop  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]
  running_sum_strides_cs  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]
     running_sum_strides  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]
        running_sum_conv  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]
          running_sum_nb  [31 34 39 53 65 76 79 78 72 66 60 58 60 66 72 78 77 75 68 63 55 47 29 13]

针对不同的输入大小对所有这些进行基准测试:

import pandas as pd


timeds_n = {}
for p in range(6):
    n = 10 ** p
    k = 3
    arr = np.array(load.tolist() * n)
    print(f"N = {n * len(load)}")
    base = funcs[0](arr, k)
    timeds_n[n] = []
    for func in funcs:
        res = func(arr, k)
        timed = %timeit -r 8 -n 8 -q -o func(arr, k)
        timeds_n[n].append(timed.best)
        print(f"{func.__name__:>24}  {np.allclose(base, res)}  {timed.best:.9f}")


pd.DataFrame(data=timeds_n, index=[func.__name__ for func in funcs]).transpose().plot()

和变化 k:

timeds_k = {}
for p in range(1, 10):
    n = 10 ** 5
    k = 2 ** p
    arr = np.array(load.tolist() * n)
    print(f"k = {k}")
    timeds_k[k] = []
    base = funcs[0](arr, k)
    for func in funcs:
        res = func(arr, k)
        timed = %timeit -q -o func(arr, k)
        timeds_k[k].append(timed.best)
        print(f"{func.__name__:>24}  {np.allclose(base, res)}  {timed.best:.9f}")


pd.DataFrame(data=timeds_k, index=[func.__name__ for func in funcs]).transpose().plot()