不使用计数和数据总和如何计算移动平均?

173

我正在尝试找到一种在不存储迄今为止收到的数据的总计数和总和数据的情况下,计算移动累积平均值的方法。

我提出了两种算法,但都需要存储计数:

  • 新平均值=(旧计数 * 旧数据 + 下一个数据)/ 下一个计数
  • 新平均值=旧平均值+(下一个数据-旧平均值)/ 下一个计数

这些方法的问题是计数会变得越来越大,导致结果平均值的精度丢失。

第一种方法使用旧计数和下一个计数,它们显然相差1。这让我想到可能有一种方法可以消除计数,但不幸的是我还没有找到。它确实使我进一步思考,结果得到了第二种方法,但仍存在计数。

这是可能的吗,还是我在寻找不可能的事情?


3
请注意,在数值上,存储当前总数和当前计数是最稳定的方式。否则,对于更高的计数,下一个/(下一个计数)将开始下溢。因此,如果你真的担心失去精度,请保留总数! - AlexR
3
请参阅维基百科https://en.wikipedia.org/wiki/Moving_average - xmedeko
9个回答

110
您可以简单地执行以下操作:

double approxRollingAverage (double avg, double new_sample) {

    avg -= avg / N;
    avg += new_sample / N;

    return avg;
}

其中N是您想要平均值的样本数。 请注意,此近似等同于指数移动平均值。 参见:在C++中计算滚动/移动平均值


6
在这行代码之前,你不需要将N加1吗? avg += new_sample / N; - Damian
27
这并不完全正确。@Muis所描述的是指数加权移动平均,有时候是适用的,但并不完全符合OP的要求。例如,假设大多数点都在2到4范围内,但一个值超过了一百万,期望得到的行为是什么?指数加权移动平均将会在相当长的时间内保留那个一百万的痕迹。而OP提出的有限卷积,则会在N步之后立即丢失这个值。它的优点是存储恒定。 - jma
12
那不是一个移动平均。你描述的是一种单极滤波器,它对信号跳跃产生指数响应。移动平均创建一个长度为N的线性响应。 - ruhig brauner
5
请注意,这与平均数的通常定义相差甚远。如果你将N设为5,并输入5个数字“5”,那么平均值将是0.67。 - Dan Dascalescu
4
假设avg被初始化为0,那么这句话的意思是:如果你将avg初始化为第一个元素,它的表现会更好。在你的例子中,结果将一直是5 - Dimagog
显示剩余3条评论

103
New average = old average * (n-1)/n + new value /n

这是在假设计数只改变了一个值的情况下。如果它被M个值更改,则:

new average = old average * (n-len(M))/n + (sum of values in M)/n).

这是数学公式(我相信这是最有效的),相信你们可以自己进一步编写代码


20
第一个公式稍微更有效率:new_average = (old_average * (n-1) + new_value) / n -- 去掉了一个除号。 - Pixelstix
2
当我实现这个方程时,运行平均值总是缓慢增加。它从不下降 - 只会上升。 - fIwJlxSzApHEZIl
“n” 是在添加新元素之后的总数还是在添加新元素之前的总数? - David Callanan
@Pixelstix,你的解决方案完全有效,但请记住,它比原始解决方案更容易溢出。根据随时间推移溢出的可能性,人们应该选择其中一种方法。 - Gili
@DavidCallanan 在这个例子中,之后。我同意这样有点尴尬。你需要稍微调整一下公式,使用一个n,它是新元素之前的总数。 - Pixelstix
显示剩余4条评论

44

这是另一个对Muis、Abdullah Al-Ageel和Flip的回答提供评论的答案,它们在数学上是相同的,只是写法不同。

当然,我们有José Manuel Ramos的分析,解释了舍入误差如何稍微地影响每一个,但这取决于每个答案如何应用于代码,因此是实施相关的。

然而,确实存在一个很大的区别

它在Muis的N,Flip的k和Abdullah Al-Ageel的n中。Abdullah Al-Ageel没有完全解释n应该是什么,但Nk的不同之处在于N是“要进行平均化的样本数”,而k则是采样值的计数。(尽管我怀疑将N称为“样本数量”是否准确。)

然后我们来到下面的答案。它本质上是同其他答案一样的指数加权移动平均,因此如果您正在寻找替代方案,请在此停止。

指数加权移动平均

最初:

average = 0
counter = 0

对于每个值:

counter += 1
average = average + (value - average) / min(counter, FACTOR)

区别在于min(counter, FACTOR)部分。这相当于说min(Flip的k,Muis的N)

FACTOR是一个常数,影响平均值如何迅速地“赶上”最新趋势。数字越小,速度越快。 (在1时它不再是平均值,只成为最新值。)

这个答案需要运行计数器counter。如果有问题,min(counter, FACTOR)可以替换为只是FACTOR,变成Muis的答案。这样做的问题是移动平均受到average最初化的影响。如果它被初始化为0,那么这个零可能需要很长时间才能消失在平均值中。

最终效果

指数移动平均


6
解释清楚了。我只是觉得你的图表中缺少一个平均值,因为那是 OP 所询问的。 - xmedeko
也许我漏看了什么,但您是否意思是max(counter, FACTOR)min(counter, FACTOR)将总是返回FACTOR,对吧? - WebWanderer
1
我相信 min(counter, FACTOR) 的目的是考虑预热期。如果没有它,假设你的因子(或N或所需采样次数)为1000,则在获得准确结果之前至少需要1000个样本,因为此前的所有更新都会假定你拥有1000个样本,而实际上你只有20个。 - rharter
1
达到因子后停止计数会更好,这样可能会更快。 - inf3rno
1
指数加权移动平均实际上只是一个糟糕的无限脉冲响应(IIR)低通滤波器。最好实现一个适当的一阶Butterworth IIR。我需要再次检查,但我模糊地记得指数加权移动平均的增益不是单位增益,不像Butterworth IIR。 - Flip

43

4
这与Muis所实现的类似,只是使用了一个公因数进行除法计算。因此只需进行一次除法运算。 - Flip
2
实际上,它更接近于@Abdullah-Al-Ageel(基本上是可交换的数学),因为Muis没有考虑递增N;复制粘贴公式参考:[n处的平均值] = [n-1处的平均值] +(x-[n-1处的平均值])/ n - drzaus
3
@Flip & drwaus:难道Muis和Abdullah Al-Ageel的解决方案不是完全一样吗?它们进行了相同的计算,只是书写方式不同。对我来说,这3个答案是相同的,只是这个答案更加直观(遗憾的是我们无法在SO上使用MathJax)。 - user276648

13

一个使用JavaScript的例子,供比较:

https://jsfiddle.net/drzaus/Lxsa4rpz/

function calcNormalAvg(list) {
    // sum(list) / len(list)
    return list.reduce(function(a, b) { return a + b; }) / list.length;
}
function calcRunningAvg(previousAverage, currentNumber, index) {
    // [ avg' * (n-1) + x ] / n
    return ( previousAverage * (index - 1) + currentNumber ) / index;
}

(function(){
  // populate base list
var list = [];
function getSeedNumber() { return Math.random()*100; }
for(var i = 0; i < 50; i++) list.push( getSeedNumber() );

  // our calculation functions, for comparison
function calcNormalAvg(list) {
   // sum(list) / len(list)
 return list.reduce(function(a, b) { return a + b; }) / list.length;
}
function calcRunningAvg(previousAverage, currentNumber, index) {
   // [ avg' * (n-1) + x ] / n
 return ( previousAverage * (index - 1) + currentNumber ) / index;
}
  function calcMovingAvg(accumulator, new_value, alpha) {
   return (alpha * new_value) + (1.0 - alpha) * accumulator;
}

  // start our baseline
var baseAvg = calcNormalAvg(list);
var runningAvg = baseAvg, movingAvg = baseAvg;
console.log('base avg: %d', baseAvg);
  
  var okay = true;
  
  // table of output, cleaner console view
  var results = [];

  // add 10 more numbers to the list and compare calculations
for(var n = list.length, i = 0; i < 10; i++, n++) {
 var newNumber = getSeedNumber();

 runningAvg = calcRunningAvg(runningAvg, newNumber, n+1);
 movingAvg = calcMovingAvg(movingAvg, newNumber, 1/(n+1));

 list.push(newNumber);
 baseAvg = calcNormalAvg(list);

 // assert and inspect
 console.log('added [%d] to list at pos %d, running avg = %d vs. regular avg = %d (%s), vs. moving avg = %d (%s)'
  , newNumber, list.length, runningAvg, baseAvg, runningAvg == baseAvg, movingAvg, movingAvg == baseAvg
 )
results.push( {x: newNumber, n:list.length, regular: baseAvg, running: runningAvg, moving: movingAvg, eqRun: baseAvg == runningAvg, eqMov: baseAvg == movingAvg } );

if(runningAvg != baseAvg) console.warn('Fail!');
okay = okay && (runningAvg == baseAvg);    
}
  
  console.log('Everything matched for running avg? %s', okay);
  if(console.table) console.table(results);
})();


13

相比于Muis方法,Flip方法在计算上更加一致。

使用双精度浮点数格式,您可以看到Muis方法中的四舍五入问题:

Muis method

当您进行除法和减法时,先前存储的值中会出现四舍五入,从而改变它。

然而,Flip方法保留了存储的值,并减少了除法次数,因此减小了舍入误差,并将误差最小化传播到存储的值。只有在有东西可添加时(当N很大时没有东西可添加),才会引起舍入错误。

Flip method

当您使大值的均值趋近于零时,这些更改是显著的。

我用电子表格程序向您展示了所得结果:

首先,所获得的结果:Results

A和B列分别是n和X_n值。

C列是Flip方法,D列是Muis方法,在均值中存储的结果。E列对应于计算中使用的中间值。

下面是显示偶数值平均值的图形:

Graph

如您所见,两种方法之间存在很大的差异。


4
不算是答案,但有用的信息。如果您在图表中添加第三行来显示过去n个值的真实平均数,那就更好了,这样我们就能看到哪种方法更接近真实情况了。 - jpaugh
4
B列在-1.00E+15和1.00E+15之间交替变化,因此当N为偶数时,实际平均值应为0。图表标题为“偶数部分均值”。这意味着你询问的第三行简单地是f(x)=0。该图显示两种方法都引入了错误,这些错误不断增加。 - desowin
没错,该图表准确地显示了使用两种方法进行计算时涉及大数的误差传播。 - José Manuel Ramos
2
你的图例颜色有误:Muis 的应该是橙色,Flip 的应该是蓝色。 - xmedeko

7
一个基于以上答案的简洁Python解决方案:
class RunningAverage():
    def __init__(self):
        self.average = 0
        self.n = 0
        
    def __call__(self, new_value):
        self.n += 1
        self.average = (self.average * (self.n-1) + new_value) / self.n 
        
    def __float__(self):
        return self.average
    
    def __repr__(self):
        return "average: " + str(self.average)

使用方法:

x = RunningAverage()
x(0)
x(2)
x(4)
print(x)

2

在Java8中:

LongSummaryStatistics movingAverage = new LongSummaryStatistics();
movingAverage.accept(new data);
...
average = movingAverage.getAverage();

您还可以使用 IntSummaryStatisticsDoubleSummaryStatistics ...等。


5
OP正在寻求一种算法,而不是如何在Java中计算的指针。 - olq_plo

0
(OldAvgValue * OldCount + NewValue * NewCount)/(NewCount + OldCount)
例如:平均价格为20个项目为10,需要再添加15个项目,价格为10.5 那么,(20 * 10 + 15 * 10.5)/(20 + 15)= 10.21
下一个周期如果您想再添加5个项目,价格为11,那么计算公式为: (35 * 10.21 + 5 * 11)/(35 + 5)= 10.30

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接