web-dev-qa-db-fra.com

Comment calculer la moyenne mobile sans conserver le nombre et le total des données?

J'essaie de trouver un moyen de calculer une moyenne cumulative mobile sans stocker le nombre et le total des données reçues jusqu'à présent.

Je suis venu avec deux algorithmes mais les deux ont besoin de stocker le compte:

  • nouvelle moyenne = ((ancien nombre * anciennes données) + données suivantes)/prochain nombre
  • nouvelle moyenne = ancienne moyenne + (données suivantes - ancienne moyenne)/nombre suivant

Le problème avec ces méthodes est que le nombre devient de plus en plus grand, ce qui entraîne une perte de précision dans la moyenne résultante.

La première méthode utilise l’ancien compte et le compte suivant, qui sont évidemment égaux à 1. Cela m'a fait penser qu'il est peut-être possible de supprimer le compte, mais malheureusement je ne l'ai pas encore trouvé. Cela m'a cependant amené un peu plus loin, ce qui a abouti à la deuxième méthode, mais le compte est toujours présent.

Est-ce possible ou suis-je en train de chercher l'impossible?

97
user1705674

Vous pouvez simplement faire:

double approxRollingAverage (double avg, double new_sample) {

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

    return avg;
}

N est le nombre d'échantillons pour lesquels vous souhaitez établir une moyenne. Notez que cette approximation est équivalente à une moyenne mobile exponentielle. Voir: Calculer la moyenne roulante/mobile en C++

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

Cela suppose que le nombre ne soit modifié que par une valeur. Dans le cas où il est modifié par M valeurs, alors:

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

C’est la formule mathématique (je crois la plus efficace), croyez que vous pouvez faire plus de code par vous-même

70

À partir de n blog sur les calculs de variance d'échantillon en cours, où la moyenne est également calculée à l'aide de méthode de Welford :

enter image description here

Dommage que nous ne puissions pas télécharger des images SVG.

25
Flip

Voici encore une autre réponse offrant des commentaires sur la façon dont Muis, = Abdullah Al-Ageel et Flip La réponse est mathématiquement la même chose sauf écrit différemment.

Certes, nous avons l’analyse de José Manuel Ramos expliquant comment les erreurs d’arrondi affectent chaque différend légèrement, mais cela dépend de l’implémentation et peut changer en fonction de la manière dont chaque réponse est appliquée au code.

Il y a cependant une assez grande différence

C'est dans Muis's N, Flip's k, et Abdullah Al-Ageel 's n. Abdullah Al-Ageel n'explique pas vraiment ce que devrait être n, mais N et k diffèrent par le fait que N est " le nombre d'échantillons sur lequel vous voulez faire la moyenne" tandis que k est le nombre de valeurs échantillonnées. (Bien que je doute de savoir si l'appel de N le nombre d'échantillons est exact.)

Et nous arrivons à la réponse ci-dessous. C'est essentiellement le même vieux moyenne mobile pondérée exponentielle que les autres, donc si vous cherchez une alternative, arrêtez ici.

Moyenne mobile pondérée exponentielle

Initialement:

average = 0
counter = 0

Pour chaque valeur:

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

La différence est la partie min(counter, FACTOR). Cela revient à dire min(Flip's k, Muis's N).

FACTOR est une constante qui affecte la rapidité avec laquelle la moyenne "rattrape" la dernière tendance. Plus le nombre est petit, plus vite. (À 1, Ce n'est plus une moyenne et devient simplement la dernière valeur.)

Cette réponse nécessite le compteur en cours d'exécution counter. Si problématique, la min(counter, FACTOR) peut être remplacée par la simple FACTOR, en la transformant en réponse Muis. Le problème avec ceci est que la moyenne mobile est affectée par tout ce à quoi average est initialisé. S'il a été initialisé à 0, Ce zéro peut prendre beaucoup de temps pour sortir de la moyenne.

Comment ça finit par regarder

Exponential moving average

11
antak

La réponse de Flip est numériquement plus cohérente que celle de Muis.

En utilisant le format à double numéro, vous pouvez voir le problème d'arrondi dans l'approche Muis:

The Muis approach

Lorsque vous divisez et soustrayez, un arrondi apparaît dans la valeur stockée précédente, en le modifiant.

Cependant, l'approche Flip préserve la valeur stockée et réduit le nombre de divisions, réduisant ainsi l'arrondi et minimisant l'erreur propagée à la valeur stockée. Ajouter seulement fera apparaître des arrondis s'il y a quelque chose à ajouter (quand N est grand, il n'y a rien à ajouter)

The Flip approach

Ces changements sont remarquables lorsque vous faites une moyenne de grandes valeurs tendent leur moyenne à zéro.

Je vous montre les résultats à l'aide d'un tableur:

Premièrement, les résultats obtenus: Results

Les colonnes A et B sont les valeurs n et X_n, respectivement.

La colonne C correspond à l’approche Flip et la première à l’approche Muis, le résultat stocké dans la moyenne. La colonne E correspond à la valeur moyenne utilisée dans le calcul.

Le graphique suivant montre la moyenne des valeurs paires:

Graph

Comme vous pouvez le constater, il existe de grandes différences entre les deux approches.

7
José Manuel Ramos

Un exemple utilisant javascript, à titre de comparaison:

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);
})();
4
drzaus

En Java8:

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

vous avez aussi IntSummaryStatistics, DoubleSummaryStatistics ...

0
jmhostalet