如何使用NumPy计算移动平均线?

gon*_*opp 84 python numpy time-series scipy

似乎没有简单计算numpy/scipy上的移动平均值的函数,导致复杂的解决方案.

我的问题是双重的:

  • 用numpy(正确)实现移动平均线的最简单方法是什么?
  • 由于这看起来非常重要且容易出错,因此有充分的理由不在这种情况下包含电池吗?

Jai*_*ime 137

如果你只是想要一个简单的非加权移动平均线,您可以轻松地实现它np.cumsum,这可能 比基于FFT方法快:

编辑纠正了代码中Bean发现的一个错误的索引.编辑

def moving_average(a, n=3) :
    ret = np.cumsum(a, dtype=float)
    ret[n:] = ret[n:] - ret[:-n]
    return ret[n - 1:] / n

>>> a = np.arange(20)
>>> moving_average(a)
array([  1.,   2.,   3.,   4.,   5.,   6.,   7.,   8.,   9.,  10.,  11.,
        12.,  13.,  14.,  15.,  16.,  17.,  18.])
>>> moving_average(a, n=4)
array([  1.5,   2.5,   3.5,   4.5,   5.5,   6.5,   7.5,   8.5,   9.5,
        10.5,  11.5,  12.5,  13.5,  14.5,  15.5,  16.5,  17.5])
Run Code Online (Sandbox Code Playgroud)

所以我猜答案是:它实现起来非常简单,而且numpy可能已经变得有点臃肿了.

  • 嗯,似乎这个"易于实现"的功能实际上很容易出错,并促进了对内存效率的良好讨论.如果这意味着知道某事做得对,我很高兴有臃肿. (35认同)
  • @Timmmm我做了,这确实是个问题.这个答案背后的一般原理被广泛用于图像处理(他们称之为求和区域表),因此问题必须在实现中.一个很好的例子就是过早优化,因为我有点回想起就地进行操作"因为它会更有效率." 从好的方面来看,它可能会更快地产生错误的答案...... (7认同)
  • 此代码不正确.例如moving_average([1,2,5,10],n = 2)给出[1.,3.5,8.5].即使回答者的测试案例的移动平均值从0到19也是不正确的,声称0,1和2的平均值是0.5.怎么得到6票? (6认同)
  • 感谢您的错误检查,它现在似乎工作正常。至于赞成票,我猜答案背后的总体思路比实现中的一个错误更重要,但谁知道呢。 (5认同)
  • 是否可以/应该更新此答案以包括 numpy 中引入的 [`numpy.lib.stride_tricks.sliding_window_view`](https://numpy.org/devdocs/reference/ generated/numpy.lib.stride_tricks.sliding_window_view.html) v1.20?解决方案变成了一句:“sliding_window_view(array, window_size).mean(axis=-1)”。速度可能与“cumsum”相当,但我不能 100% 确定。 (5认同)
  • 我不明白为什么就地减法不起作用. (4认同)
  • 我发现了这个问题.`ret [n:] - = ret [: - n]`与`ret [n:] = ret [n:] - ret [: - n]`不同.我在这个答案中修改了代码.编辑:没有其他人只是打败了我. (2认同)
  • @askewchan在Timmmm的例子中,当`ret [2:] - = ret [: - 2]`时,`ret`是`[1,2,3,4,5,6]`.当它处理最后一个数据点时,它基本上是`ret [5] - = ret [3]`.我们想得到`ret [5] = 6 - 4`,但我们得到`ret [5] = 6 - 2`,因为`ret [3]`已经被就地操作修改为`ret [3] - = ret [1]`,即`2`.如果你不进行就地操作,它会创建一个临时数组,然后将其复制到`ret`,避免这个问题. (2认同)
  • @ user391339这取决于你的目标.如果您从最后删除时间,您的条目将在未来平均.如果您从头开始删除时间,它们将是过去值的平均值.你甚至可以删除一半和一半,并有一个居中的平均值. (2认同)

dou*_*oug 73

NumPy缺乏特定的特定于域的功能可能是由于Core Team的纪律和对NumPy主要指令的保真度:提供N维数组类型,以及创建和索引这些数组的函数.像许多基本目标一样,这个目标并不小,NumPy非常出色.

(更大)SciPy包含更大的域特定库集合(SciPy开发人员称为子包) - 例如,数值优化(优化),信号处理(信号)和积分微积分(积分).

我的猜测是你所追求的功能至少在一个SciPy子包中(或许是scipy.signal); 然而,我会先看看SciPy scikits的集合,找出相关的scikit(s)并寻找那里感兴趣的功能.

Scikits是基于NumPy/SciPy独立开发的包,并针对特定的技术学科(例如,scikits-image,scikits-learn等)其中一些(特别是用于数值优化的令人敬畏的OpenOpt)受到高度重视,成熟的项目早在选择居住在相对较新的scikits标题之前.该Scikits主页喜欢约30个这样的上述清单scikits,但至少数那些正在积极发展不再.

遵循这个建议会引导你到scikits-timeseries ; 但是,这个包裹不再处于积极发展之中; 实际上,Pandas已成为AFAIK,事实上 基于NumPy的时间序列库.

熊猫有几个可用于计算移动平均值的函数; 其中最简单的可能就是rolling_mean,你可以这样使用:

>>> # the recommended syntax to import pandas
>>> import pandas as PD
>>> import numpy as NP

>>> # prepare some fake data:
>>> # the date-time indices:
>>> t = PD.date_range('1/1/2010', '12/31/2012', freq='D')

>>> # the data:
>>> x = NP.arange(0, t.shape[0])

>>> # combine the data & index into a Pandas 'Series' object
>>> D = PD.Series(x, t)
Run Code Online (Sandbox Code Playgroud)

现在,只需调用函数rolling_mean传递Series对象和窗口大小,在下面的示例中为10天.

>>> d_mva = PD.rolling_mean(D, 10)

>>> # d_mva is the same size as the original Series
>>> d_mva.shape
    (1096,)

>>> # though obviously the first w values are NaN where w is the window size
>>> d_mva[:3]
    2010-01-01         NaN
    2010-01-02         NaN
    2010-01-03         NaN
Run Code Online (Sandbox Code Playgroud)

验证它是否有效 - 例如,比较原始系列中的值10 - 15与使用滚动平均值平滑的新系列

>>> D[10:15]
     2010-01-11    2.041076
     2010-01-12    2.041076
     2010-01-13    2.720585
     2010-01-14    2.720585
     2010-01-15    3.656987
     Freq: D

>>> d_mva[10:20]
      2010-01-11    3.131125
      2010-01-12    3.035232
      2010-01-13    2.923144
      2010-01-14    2.811055
      2010-01-15    2.785824
      Freq: D
Run Code Online (Sandbox Code Playgroud)

Rolling_mean函数以及大约十几个其他函数在Rubric 移动窗口函数下的Pandas文档中非正式地分组; Pandas中第二个相关的函数组称为指数加权函数(例如,ewma,它计算指数移动的加权平均值).第二组未包含在第一组(移动窗口函数)中的事实可能是因为指数加权变换不依赖于固定长度的窗口

  • 我怀疑计算移动平均线是对OP或其他任何人的孤立要求.如果您需要计算移动平均线,那么您几乎肯定会有一个时间序列,这意味着您需要一个数据结构,允许您将日期时间索引符合您的数据,这就是您所指的"开销". (5认同)
  • Pandas确实拥有强大的移动窗口功能.但在我看来,对于一个简单的移动平均线来说,它有点过多的开销. (4认同)
  • 只是想补充说,移动平均功能已被提取到[Bottleneck](http://berkeleyanalytics.com/bottleneck/)库中,如果pandas看起来太重了作为依赖. (3认同)
  • 首先,感谢您抽出宝贵的时间来写这个非常有用的答案。确实,我看不到不涉及时间序列的移动平均线的用途。但这并不意味着您需要使其符合日期时间,甚至不需要符合特定的采样间隔(可能未知)。 (2认同)
  • 'rolling_mean'不再是pf熊猫的一部分,请查看使用'rolling'的回复 (2认同)

arg*_*m2f 38

这里有多种方法可以做到这一点,以及一些基准。最好的方法是使用来自其他库的优化代码的版本。该bottleneck.move_mean方法可能是最好的。该scipy.convolve方法也非常快速、可扩展,并且在语法和概念上都很简单,但对于非常大的窗口值不能很好地扩展。numpy.cumsum如果您需要纯numpy方法,该方法很好。

注意:其中一些(例如bottleneck.move_mean)不居中,并且会移动您的数据。

import numpy as np
import scipy as sci
import scipy.signal as sig
import pandas as pd
import bottleneck as bn
import time as time

def rollavg_direct(a,n): 
    'Direct "for" loop'
    assert n%2==1
    b = a*0.0
    for i in range(len(a)) :
        b[i]=a[max(i-n//2,0):min(i+n//2+1,len(a))].mean()
    return b

def rollavg_comprehension(a,n):
    'List comprehension'
    assert n%2==1
    r,N = int(n/2),len(a)
    return np.array([a[max(i-r,0):min(i+r+1,N)].mean() for i in range(N)]) 

def rollavg_convolve(a,n):
    'scipy.convolve'
    assert n%2==1
    return sci.convolve(a,np.ones(n,dtype='float')/n, 'same')[n//2:-n//2+1]  

def rollavg_convolve_edges(a,n):
    'scipy.convolve, edge handling'
    assert n%2==1
    return sci.convolve(a,np.ones(n,dtype='float'), 'same')/sci.convolve(np.ones(len(a)),np.ones(n), 'same')  

def rollavg_cumsum(a,n):
    'numpy.cumsum'
    assert n%2==1
    cumsum_vec = np.cumsum(np.insert(a, 0, 0)) 
    return (cumsum_vec[n:] - cumsum_vec[:-n]) / n

def rollavg_cumsum_edges(a,n):
    'numpy.cumsum, edge handling'
    assert n%2==1
    N = len(a)
    cumsum_vec = np.cumsum(np.insert(np.pad(a,(n-1,n-1),'constant'), 0, 0)) 
    d = np.hstack((np.arange(n//2+1,n),np.ones(N-n)*n,np.arange(n,n//2,-1)))  
    return (cumsum_vec[n+n//2:-n//2+1] - cumsum_vec[n//2:-n-n//2]) / d

def rollavg_roll(a,n):
    'Numpy array rolling'
    assert n%2==1
    N = len(a)
    rolling_idx = np.mod((N-1)*np.arange(n)[:,None] + np.arange(N), N)
    return a[rolling_idx].mean(axis=0)[n-1:] 

def rollavg_roll_edges(a,n):
    # see /sf/ask/2947075771/
    'Numpy array rolling, edge handling'
    assert n%2==1
    a = np.pad(a,(0,n-1-n//2), 'constant')*np.ones(n)[:,None]
    m = a.shape[1]
    idx = np.mod((m-1)*np.arange(n)[:,None] + np.arange(m), m) # Rolling index
    out = a[np.arange(-n//2,n//2)[:,None], idx]
    d = np.hstack((np.arange(1,n),np.ones(m-2*n+1+n//2)*n,np.arange(n,n//2,-1)))
    return (out.sum(axis=0)/d)[n//2:]

def rollavg_pandas(a,n):
    'Pandas rolling average'
    return pd.DataFrame(a).rolling(n, center=True, min_periods=1).mean().to_numpy()

def rollavg_bottlneck(a,n):
    'bottleneck.move_mean'
    return bn.move_mean(a, window=n, min_count=1)

N = 10**6
a = np.random.rand(N)
functions = [rollavg_direct, rollavg_comprehension, rollavg_convolve, 
        rollavg_convolve_edges, rollavg_cumsum, rollavg_cumsum_edges, 
        rollavg_pandas, rollavg_bottlneck, rollavg_roll, rollavg_roll_edges]

print('Small window (n=3)')
%load_ext memory_profiler
for f in functions : 
    print('\n'+f.__doc__+ ' : ')
    %timeit b=f(a,3)

print('\nLarge window (n=1001)')
for f in functions[0:-2] : 
    print('\n'+f.__doc__+ ' : ')
    %timeit b=f(a,1001)

print('\nMemory\n')
print('Small window (n=3)')
N = 10**7
a = np.random.rand(N)
%load_ext memory_profiler
for f in functions[2:] : 
    print('\n'+f.__doc__+ ' : ')
    %memit b=f(a,3)

print('\nLarge window (n=1001)')
for f in functions[2:-2] : 
    print('\n'+f.__doc__+ ' : ')
    %memit b=f(a,1001)
Run Code Online (Sandbox Code Playgroud)

定时,小窗口 (n=3)

Direct "for" loop : 

4.14 s ± 23.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

List comprehension : 
3.96 s ± 27.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

scipy.convolve : 
1.07 ms ± 26.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

scipy.convolve, edge handling : 
4.68 ms ± 9.69 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

numpy.cumsum : 
5.31 ms ± 5.11 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

numpy.cumsum, edge handling : 
8.52 ms ± 11.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Pandas rolling average : 
9.85 ms ± 9.63 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

bottleneck.move_mean : 
1.3 ms ± 12.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Numpy array rolling : 
31.3 ms ± 91.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy array rolling, edge handling : 
61.1 ms ± 55.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

时序,大窗口 (n=1001)

Direct "for" loop : 
4.67 s ± 34 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

List comprehension : 
4.46 s ± 14.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

scipy.convolve : 
103 ms ± 165 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

scipy.convolve, edge handling : 
272 ms ± 1.23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

numpy.cumsum : 
5.19 ms ± 12.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

numpy.cumsum, edge handling : 
8.7 ms ± 11.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Pandas rolling average : 
9.67 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

bottleneck.move_mean : 
1.31 ms ± 15.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Run Code Online (Sandbox Code Playgroud)

内存,小窗口 (n=3)

The memory_profiler extension is already loaded. To reload it, use:
  %reload_ext memory_profiler

scipy.convolve : 
peak memory: 362.66 MiB, increment: 73.61 MiB

scipy.convolve, edge handling : 
peak memory: 510.24 MiB, increment: 221.19 MiB

numpy.cumsum : 
peak memory: 441.81 MiB, increment: 152.76 MiB

numpy.cumsum, edge handling : 
peak memory: 518.14 MiB, increment: 228.84 MiB

Pandas rolling average : 
peak memory: 449.34 MiB, increment: 160.02 MiB

bottleneck.move_mean : 
peak memory: 374.17 MiB, increment: 75.54 MiB

Numpy array rolling : 
peak memory: 661.29 MiB, increment: 362.65 MiB

Numpy array rolling, edge handling : 
peak memory: 1111.25 MiB, increment: 812.61 MiB
Run Code Online (Sandbox Code Playgroud)

内存,大窗口 (n=1001)

scipy.convolve : 
peak memory: 370.62 MiB, increment: 71.83 MiB

scipy.convolve, edge handling : 
peak memory: 521.98 MiB, increment: 223.18 MiB

numpy.cumsum : 
peak memory: 451.32 MiB, increment: 152.52 MiB

numpy.cumsum, edge handling : 
peak memory: 527.51 MiB, increment: 228.71 MiB

Pandas rolling average : 
peak memory: 451.25 MiB, increment: 152.50 MiB

bottleneck.move_mean : 
peak memory: 374.64 MiB, increment: 75.85 MiB
Run Code Online (Sandbox Code Playgroud)


yat*_*atu 26

一种简单的方法是使用np.convolve。其背后的想法是利用离散卷积的计算方式,并使用它返回滚动平均值。这可以通过np.ones对长度等于我们想要的滑动窗口长度的序列进行卷积来完成。

为此,我们可以定义以下函数:

def moving_average(x, w):
    return np.convolve(x, np.ones(w), 'valid') / w
Run Code Online (Sandbox Code Playgroud)

该函数将对序列x和长度为1的序列进行卷积w。请注意,选择的mode方式valid是仅对序列完全重叠的点给出卷积。


用例

一些例子:

x = np.array([5,3,8,10,2,1,5,1,0,2])
Run Code Online (Sandbox Code Playgroud)

对于具有窗口长度的移动平均值,2我们将有:

moving_average(x, 2)
# array([4. , 5.5, 9. , 6. , 1.5, 3. , 3. , 0.5, 1. ])
Run Code Online (Sandbox Code Playgroud)

对于一个长度的窗口4

moving_average(x, 4)
# array([6.5 , 5.75, 5.25, 4.5 , 2.25, 1.75, 2.  ])
Run Code Online (Sandbox Code Playgroud)

细节

让我们更深入地了解离散卷积的计算方式。以下功能旨在复制np.convolve计算输出值的方式:

def mov_avg(x, w):
    for m in range(len(x)-(w-1)):
        yield sum(np.ones(w) * x[m:m+w]) / w 
Run Code Online (Sandbox Code Playgroud)

对于上面的相同示例,这还将产生:

list(mov_avg(x, 2))
# [4.0, 5.5, 9.0, 6.0, 1.5, 3.0, 3.0, 0.5, 1.0]
Run Code Online (Sandbox Code Playgroud)

因此,在每个步骤中要做的就是获取1的数组与当前窗口之间的内积。在这种情况下,乘以np.ones(w)是多余的,因为我们直接取sum序列的。

贝娄是一个示例,该示例说明了如何计算第一个输出,以便更加清晰。假设我们需要一个窗口w=4

[1,1,1,1]
[5,3,8,10,2,1,5,1,0,2]
= (1*5 + 1*3 + 1*8 + 1*10) / w = 6.5
Run Code Online (Sandbox Code Playgroud)

并且以下输出将计算为:

  [1,1,1,1]
[5,3,8,10,2,1,5,1,0,2]
= (1*3 + 1*8 + 1*10 + 1*2) / w = 5.75
Run Code Online (Sandbox Code Playgroud)

依此类推,一旦执行了所有重叠操作,就返回序列的移动平均值。

  • 是否应该澄清这种方法将产生“中心移动平均线”而不是通常为金融应用计算的“简单移动平均线”?https://en.wikipedia.org/wiki/Moving_average (6认同)
  • 这是个好主意!它比@Jaime 对小 n 的回答要快,但对于较大的 n 会变慢。 (2认同)
  • 有时让输出数组与输入大小相同很有用。为此,可以将 `mode='valid'` 替换为 `'same'`。正是在这种情况下,边缘点将趋向于零。 (2认同)

Vla*_*dtn 8

从上面改编了使用熊猫的答案,因为rolling_mean不再是熊猫的一部分

# the recommended syntax to import pandas
import pandas as pd
import numpy as np

# prepare some fake data:
# the date-time indices:
t = pd.date_range('1/1/2010', '12/31/2012', freq='D')

# the data:
x = np.arange(0, t.shape[0])

# combine the data & index into a Pandas 'Series' object
D = pd.Series(x, t)
Run Code Online (Sandbox Code Playgroud)

现在,只需rolling使用窗口大小在数据框上调用该函数,在我的以下示例中,该大小为10天。

d_mva10 = D.rolling(10).mean()

# d_mva is the same size as the original Series
# though obviously the first w values are NaN where w is the window size
d_mva10[:11]

2010-01-01    NaN
2010-01-02    NaN
2010-01-03    NaN
2010-01-04    NaN
2010-01-05    NaN
2010-01-06    NaN
2010-01-07    NaN
2010-01-08    NaN
2010-01-09    NaN
2010-01-10    4.5
2010-01-11    5.5
Freq: D, dtype: float64
Run Code Online (Sandbox Code Playgroud)


Kao*_*tar 8

如果有人需要一个简单的解决方案,这里有一个

def moving_average(a,n):
    N=len(a)
    return np.array([np.mean(a[i:i+n]) for i in np.arange(0,N-n+1)])
Run Code Online (Sandbox Code Playgroud)

您可以通过添加步骤参数来更改窗口之间的重叠np.arange(0,N-n+1,step)


Ant*_*nwu 7

我觉得这可以使用bottleneck轻松解决

请参阅下面的基本示例:

import numpy as np
import bottleneck as bn

a = np.random.randint(4, 1000, size=(5, 7))
mm = bn.move_mean(a, window=2, min_count=1)
Run Code Online (Sandbox Code Playgroud)

这给出了沿每个轴的移动平均值。

  • “mm”是“a”的移动平均值。

  • “窗口”是移动均值要考虑的最大条目数。

  • "min_count" 是考虑移动平均值的最小条目数(例如,对于第一个元素或数组是否具有 nan 值)。

好的部分是瓶颈有助于处理 nan 值,而且它也非常有效。


Xav*_*hot 5

从 开始Numpy 1.20sliding_window_view提供了一种在元素窗口中滑动/滚动的方法。然后您可以单独平均的窗口。

例如,对于4-element 窗口:

from numpy.lib.stride_tricks import sliding_window_view

# values = np.array([5, 3, 8, 10, 2, 1, 5, 1, 0, 2])
np.average(sliding_window_view(values, window_shape = 4), axis=1)
# array([6.5, 5.75, 5.25, 4.5, 2.25, 1.75, 2])
Run Code Online (Sandbox Code Playgroud)

注意 的中间结果sliding_window_view

# values = np.array([5, 3, 8, 10, 2, 1, 5, 1, 0, 2])
sliding_window_view(values, window_shape = 4)
# array([[ 5,  3,  8, 10],
#        [ 3,  8, 10,  2],
#        [ 8, 10,  2,  1],
#        [10,  2,  1,  5],
#        [ 2,  1,  5,  1],
#        [ 1,  5,  1,  0],
#        [ 5,  1,  0,  2]])
Run Code Online (Sandbox Code Playgroud)

  • 请注意,虽然很简单,但此方法比此处列出的其他方法要慢得多 - 在我的测试中,它比bottleneck.move_mean慢大约80倍,比numpy.cumsum慢15倍 (3认同)