Как рассчитать скользящую среднюю, не сохраняя счет и общее количество данных?

Я пытаюсь найти способ рассчитать скользящее кумулятивное среднее без сохранения количества и общих данных, полученных на данный момент.

Я придумал два алгоритма, но оба должны хранить счет:

  • новое среднее = ((старый счет * старые данные) + следующие данные)/следующий счет
  • новое среднее = старое среднее + (следующие данные - старое среднее)/следующее число

Проблема с этими методами состоит в том, что счет становится все больше и больше, что приводит к потере точности в результирующем среднем.

Первый метод использует старый счет и следующий счет, которые, очевидно, равны 1. Это заставило меня задуматься о том, что, возможно, есть способ удалить счет, но, к сожалению, я его еще не нашел. Это действительно дало мне немного больше, хотя, в результате второй метод, но все еще счет присутствует.

Возможно ли это, или я просто ищу невозможное?

Ответ 1

Вы можете просто сделать:

double approxRollingAverage (double avg, double new_sample) {

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

    return avg;
}

Где N - количество выборок, где вы хотите усреднить. Обратите внимание, что это приближение эквивалентно экспоненциальной скользящей средней. См. Расчет скользящего/скользящего среднего в C++.

Ответ 2

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

Это математическая формула (я считаю, самый эффективный), полагаю, что вы можете сделать следующий код сами.

Ответ 3

Из блога по выполнению выборочных расчетов дисперсии, где среднее значение также рассчитывается по методу Уэлфорда:

enter image description here

Жаль, что мы не можем загрузить изображения SVG.

Ответ 4

Здесь еще один ответ, предлагающий комментарий о том, что ответы Муиса, Абдуллы Аль-Агеля и Флип - математически одно и то же, за исключением того, что они написаны по-разному.

Конечно, у нас есть анализ Хосе Мануэля Рамоса, объясняющий, как ошибки округления влияют на каждую из них по-разному, но это зависит от реализации и будет меняться в зависимости от того, как каждый ответ был применен к коду.

Однако есть довольно большая разница

Это в MUIS N, Флип k, и Абдулла аль-Ageel n. Абдулла Аль-Агил не совсем объясняет, каким должно быть n, но N и k отличаются тем, что 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, этот ноль может занять много времени, чтобы выйти из среднего значения.

Как это выглядит

Exponential moving average

Ответ 5

Ответ Flip в вычислительном отношении более последовательный, чем Muis.

Используя формат двойного числа, вы можете увидеть проблему округления в подходе Muis:

The Muis approach

Когда вы делите и вычитаете, округление появляется в предыдущем сохраненном значении, изменяя его.

Однако подход Flip сохраняет сохраненное значение и уменьшает количество делений, следовательно, уменьшая округление и минимизируя ошибку, распространяющуюся до сохраненного значения. Добавление только вызовет округления, если есть что добавить (когда N большое, добавить нечего)

The Flip approach

Эти изменения замечательны, когда среднее значение для больших значений стремится к нулю.

Я показываю вам результаты, используя программу для работы с электронными таблицами:

Во-первых, полученные результаты: Results

Столбцы A и B являются значениями n и X_n соответственно.

Столбец C является подходом Flip, а столбец D - подходом Muis, результат сохраняется в среднем. Столбец E соответствует среднему значению, используемому в вычислениях.

График, показывающий среднее четных значений, следующий:

Graph

Как видите, между обоими подходами есть большие различия.

Ответ 6

Пример использования 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);
})();

Ответ 7

В Java8:

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

у вас также есть IntSummaryStatistics, DoubleSummaryStatistics...