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


119

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.

J'ai proposé deux algorithmes, mais les deux doivent stocker le décompte:

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

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

La première méthode utilise l'ancien décompte et le décompte suivant qui sont évidemment séparés de 1. Cela m'a fait penser qu'il existe peut-être un moyen de supprimer le décompte, mais malheureusement, je ne l'ai pas encore trouvé. Cela m'a fait 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 simplement à la recherche de l'impossible?


1
NB que numériquement, stocker le total actuel et le comptage actuel est le moyen le plus stable. Sinon, pour des comptes plus élevés, le prochain / (le prochain compte) commencera à déborder. Donc, si vous craignez vraiment de perdre en précision, gardez les totaux!
AlexR

Réponses:


91

Vous pouvez simplement faire:

double approxRollingAverage (double avg, double new_sample) {

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

    return avg;
}

Nest le nombre d'échantillons sur lequel vous voulez faire la moyenne. Notez que cette approximation équivaut à une moyenne mobile exponentielle. Voir: Calculer la moyenne mobile / glissante en C ++


3
Ne devez-vous pas ajouter 1 à N avant cette ligne? avg + = nouvel_échantillon / N;
Damian

20
Ce n'est pas tout à fait correct. Ce que @Muis décrit est une moyenne mobile pondérée exponentiellement, ce qui est parfois approprié mais n'est pas exactement ce que le PO a demandé. À titre d'exemple, considérez le comportement que vous attendez lorsque la plupart des points sont compris entre 2 et 4, mais qu'une valeur est supérieure à un million. Une EWMA (ici) conservera les traces de ce million pendant un certain temps. Une convolution finie, comme indiqué par OP, la perdrait immédiatement après N étapes. Il a l'avantage d'un stockage constant.
jma

9
Ce n'est pas une moyenne mobile. Ce que vous décrivez est un filtre unipolaire qui crée des réponses exponentielles aux sauts dans le signal. Une moyenne mobile crée une réponse linéaire de longueur N.
ruhig brauner

3
Attention, c'est assez loin de la définition courante de la moyenne. Si vous définissez N = 5 et entrez 5 5échantillons, la moyenne sera de 0,67.
Dan Dascalescu

2
@DanDascalescu Bien que vous ayez raison de dire qu'il ne s'agit pas réellement d'une moyenne mobile, votre valeur déclarée est décalée d'un ordre de grandeur. Avec avginitialisé à 0, vous vous retrouvez avec 3.36après 5 5s, et 4.46après 10: cpp.sh/2ryql Pour les moyennes longues, c'est certainement une approximation utile.
cincodenada

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

Cela suppose que le nombre n'a changé que d'une valeur. Dans le cas où il est modifié par des valeurs M 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êmes


Quelle est la somme de la nouvelle valeur? est-ce différent en quelque sorte de la "nouvelle valeur" dans votre formule originale?
Mikhail le

@Mikhail dans le deuxième exemple, de mnouvelles valeurs sont prises en compte dans la nouvelle moyenne. Je crois qu'il sum of new values'agit ici de la somme des mnouvelles valeurs utilisées pour calculer la nouvelle moyenne.
Patrick Goley

9
Un peu plus efficace pour le premier: new_average = (old_average * (n-1) + new_value) / n- Supprime l'un des clivages.
Pixelstix

Que diriez-vous de la moyenne courante de 3 éléments avec 6,0,0,9?
Roshan Mehta

1
Lorsque j'implémente cette équation, la valeur ou la moyenne courante augmente toujours lentement. Il ne descend jamais - seulement vers le haut.
anon58192932

30

À partir d' un blog sur l'exécution d'échantillons de calculs de variance, où la moyenne est également calculée à l'aide de la méthode de Welford :

entrez la description de l'image ici

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


3
Ceci est similaire à ce que Muis a mis en œuvre, sauf que la division est utilisée comme un facteur commun. Donc une seule division.
Retourner le

C'est en fait plus proche de @ Abdullah-Al-Ageel (mathématiques essentiellement commutatives) en ce que Muis ne tient pas compte de l'incrémentation de N; Référence de formule copier-coller: [Avg at n] = [Avg at n-1] + (x - [Avg at n-1]) / n
drzaus

2
@Flip & drwaus: Les solutions de Muis et Abdullah Al-Ageel ne sont-elles pas exactement les mêmes? C'est le même calcul, juste écrit différemment. Pour moi ces 3 réponses sont identiques, celle-ci étant plus visuelle (dommage qu'on ne puisse pas utiliser MathJax sur SO).
user276648

23

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

Bien sûr, nous avons l' analyse de José Manuel Ramos expliquant comment les erreurs d'arrondi affectent chacune légèrement différemment, mais cela dépend de l'implémentation et changerait en fonction de la façon dont chaque réponse était appliquée au code.

Il y a cependant une assez grande différence

Il est dans Muis 's N, flip ' s k, et Abdullah Al-Ageel de n. Abdullah Al-Ageel n'explique pas tout à fait ce qui ndevrait être, mais Net kdiffère en cela Nest " le nombre d'échantillons sur lesquels vous voulez faire la moyenne " tandis que kle nombre de valeurs échantillonnées. (Bien que je doute de N l' exactitude de l' appel du nombre d'échantillons .)

Et nous arrivons ici à la réponse ci-dessous. C'est essentiellement la même vieille moyenne mobile pondérée exponentielle que les autres, donc si vous cherchez une alternative, arrêtez-vous 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 min(counter, FACTOR)partie. C'est la même chose que de dire min(Flip's k, Muis's N).

FACTORest une constante qui affecte la rapidité avec laquelle la moyenne «rattrape» la dernière tendance. Plus petit est le nombre, plus vite. (À 1ce n'est plus une moyenne et devient juste la dernière valeur.)

Cette réponse nécessite le compteur en cours d'exécution counter. En cas de problème, le min(counter, FACTOR)peut être remplacé par juste FACTOR, ce qui en fait la réponse de Muis . Le problème avec cela est que la moyenne mobile est affectée par tout ce qui averageest initialisé. S'il a été initialisé à 0, ce zéro peut prendre beaucoup de temps pour sortir de la moyenne.

Comment ça finit par ressembler

Moyenne mobile exponentielle


3
Bien expliqué. Je manque juste une moyenne simple dans votre graphique, parce que c'est ce que OP a demandé.
xmedeko le

Peut-être que je manque quelque chose, mais avez-vous, par hasard, voulu dire max(counter, FACTOR). min(counter, FACTOR)retournera toujours FACTOR, non?
WebWanderer

1
Je crois que le but du min(counter, FACTOR)est de tenir compte de la période d'échauffement. Sans cela, si votre FACTOR (ou N, ou le nombre d'échantillons souhaité) est de 1000, alors vous aurez besoin d'au moins 1000 échantillons avant d'obtenir un résultat précis, car toutes les mises à jour antérieures supposeront que vous avez 1000 échantillons, alors que vous ne pouvez avoir 20.
rharter le

Ce serait bien d'arrêter de compter après avoir atteint le facteur, ce serait probablement plus rapide de cette façon.
inf3rno le

9

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

En utilisant le format de nombre double, vous pouvez voir le problème d'arrondi dans l'approche Muis:

L'approche Muis

Lorsque vous divisez et soustrayez, un arrondi apparaît dans la valeur stockée précédente, la 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. L'ajout ne fera apparaître des arrondis que s'il y a quelque chose à ajouter (lorsque N est grand, il n'y a rien à ajouter)

L'approche Flip

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

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

Tout d'abord, les résultats obtenus: Résultats

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

La colonne C est l'approche Flip, et celle D est l'approche Muis, le résultat stocké dans la moyenne. La colonne E correspond à la valeur moyenne utilisée dans le calcul.

Un graphique montrant la moyenne des valeurs paires est le suivant:

Graphique

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


2
Pas vraiment une réponse, mais des informations utiles. Ce serait encore mieux si vous ajoutiez la troisième ligne à votre graphique, pour la vraie moyenne sur n valeurs passées, afin que nous puissions voir laquelle des deux approches se rapproche le plus.
jpaugh

2
@jpaugh: La colonne B alterne entre -1,00E + 15 et 1,00E + 15, donc lorsque N est pair, la moyenne réelle doit être 0. Le titre du graphique est "Moyennes partielles paires". Cela signifie que la troisième ligne que vous demandez est simplement f (x) = 0. Le graphique montre que les deux approches introduisent des erreurs qui ne cessent d'augmenter.
desowin

C'est exact, le graphique montre exactement l'erreur propagée à l'aide de grands nombres impliqués dans les calculs utilisant les deux approches.
José Manuel Ramos

La légende de votre graphique a de mauvaises couleurs: celle de Muis est orange, celle de Flip est bleue.
xmedeko

6

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;
}


1

En Java8:

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

vous avez aussi IntSummaryStatistics, DoubleSummaryStatistics...


2
OP demande un algorithme, pas un pointeur sur la façon de calculer cela en Java.
olq_plo

0

Une solution Python soignée basée sur les réponses ci-dessus:

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)

usage:

x = RunningAverage()
x(0)
x(2)
x(4)
print(x)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.