如何计算移动平均线,而不保持计数和数据的总和?

我试图找到一种方法来计算移动累积平均数,而不存储迄今为止收到的计数和总数据。

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

  • New average = ((旧数 * 旧数据) + next data)/next count
  • New average = old average + (next data-old average)/next count

这些方法的问题是,计数越来越大,导致在结果的平均精度损失。

第一个方法使用旧的计数器和下一个计数器,它们之间明显是1。这让我想到,也许有一种方法可以消除计数,但不幸的是,我还没有找到它。不过,它确实让我进一步了解了第二个方法,但仍然存在 count。

有可能吗,还是我只是在寻找不可能的事?

186752 次浏览

你可以简单地做:

double approxRollingAverage (double avg, double new_sample) {


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


return avg;
}

其中 N是您希望平均超过的样本数。 请注意,这种近似等效于指数移动平均值。 参见: 用 C + + 计算滚动/移动平均值

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).

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

博客运行样本方差计算,其中平均值也使用 Welford 的方法计算:

enter image description here

可惜我们不能上传 SVG 图像。

一个使用 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);
})();

Flip 的答案在计算上比 Muis 的更加一致。

使用双倍数字格式,你可以看到梅斯方法的四舍五入问题:

The Muis approach

在进行除法和减法运算时,前一个存储值中会出现一个舍入,并对其进行更改。

然而,Flip 方法保留了存储值并减少了除法的数量,从而减少了舍入,并使传播到存储值的错误最小化。如果有要添加的东西,只添加将带来舍入(当 N 很大时,没有要添加的东西)

The Flip approach

当你得到一个大值的平均值时,这些变化是显著的,它们的平均值趋于零。

我用一个电子表格程序向您展示结果:

首先,研究结果如下:Results

A 和 B 列分别是 n 和 X _ n 值。

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

下一个是显示偶数平均值的图表:

Graph

正如你所看到的,这两种方法有很大的不同。

这里还有另一个答案提供评论如何 梅斯阿卜杜拉 · 阿吉尔翻转的答案是 从数学上来说都是一样的,除了写不同。

当然,我们有 José Manuel Ramos的分析,解释舍入错误对每个错误的影响略有不同,但这取决于实现,并且会根据每个答案应用于代码的方式而改变。

然而,两者之间存在着相当大的差异

梅斯N k0的 kk1的 nk1没有完全解释 n应该是什么,但是 Nk的不同之处在于 N是“ k3”,而 k是采样值的计数。(尽管我怀疑拨打 N k4是否准确。)

现在我们来看看下面的答案。它本质上和其他的 指数加权移动平均数是一样的,所以如果你在寻找替代品,就停在这里。

指数加权移动平均

最初:

average = 0
counter = 0

对于每个值:

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

区别在于 min(counter, FACTOR)部分,这和说 min(Flip's k, Muis's N)是一样的。

FACTOR是一个常数,它影响平均“赶上”最新趋势的速度。数量越少越快。(在 1,它不再是平均值,而只是成为最新值。)

这个答案需要运行计数器 counter。如果有问题,可以用 FACTOR代替 min(counter, FACTOR),把它变成 梅斯的答案。这样做的问题是,无论 average初始化为什么,移动平均值都会受到影响。如果初始化为 0,那么这个零可能需要很长时间才能达到平均值。

看起来怎么样

Exponential moving average

在 Java8中:

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

你还有 IntSummaryStatisticsDoubleSummaryStatistics..。

基于以上答案的一个简洁的 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)