web-dev-qa-db-fra.com

Algorithme de variance glissante

J'essaie de trouver un algorithme efficace et numériquement stable pour calculer une variance glissante (par exemple, une variance sur une fenêtre glissante de 20 périodes). Je connais l'algorithme Welford qui calcule efficacement la variance courante pour un flux de nombres (il ne nécessite qu'un seul passage), mais je ne suis pas sûr qu'il puisse être adapté pour une fenêtre défilante. J'aimerais aussi que la solution évite les problèmes de précision abordés en haut de cet article de John D. Cook. Une solution dans n'importe quelle langue est bien.

60
Abiel

J'ai rencontré ce problème aussi. Il existe d'excellents messages sur le calcul de la variance cumulée courante, tels que le message Calcul exact de la variance et le message de Digital explorations de John Cooke, le code Python pour le calcul des variances de l'échantillon et de la population, la covariance et le coefficient de corrélation . Je ne pouvais pas en trouver qui soient adaptés à une fenêtre roulante.

La publication de { écart-types standard } par Subluminal Messages était essentielle pour que la formule de fenêtre glissante fonctionne. Jim prend la somme des puissances des différences au carré des valeurs par rapport à la méthode de Welford consistant à utiliser la somme des différences au carré de la moyenne. Formule comme suit: 

PSA aujourd'hui = PSA (hier) + (((x aujourd'hui * x aujourd'hui) - x hier))/n

  • x = valeur dans votre série chronologique
  • n = nombre de valeurs que vous avez analysées jusqu'à présent.

Toutefois, pour convertir la formule Power Sum Average en une variété fenêtrée, vous devez ajuster la formule comme suit: 

PSA aujourd'hui = PSA hier + (((x aujourd'hui * x aujourd'hui) - - x hier * x hier)/n 

  • x = valeur dans votre série chronologique
  • n = nombre de valeurs que vous avez analysées jusqu'à présent.

Vous aurez également besoin de la formule Rolling Simple Moving Average:

SMA aujourd'hui = SMA hier + ((x aujourd'hui - x aujourd'hui - n)/n

  • x = valeur dans votre série chronologique
  • n = période utilisée pour votre fenêtre roulante.

À partir de là, vous pouvez calculer la variance de population glissante:

Population Var aujourd'hui = (PSA aujourd'hui * n - n * SMA aujourd'hui * SMA aujourd'hui)/n

Ou la variance de roulement:

Échantillon Var aujourd'hui = (PSA aujourd'hui * n - n * SMA aujourd'hui * SMA aujourd'hui)/(n - 1)

J'ai abordé ce sujet avec des exemples de code Python dans un article de blog il y a quelques années, Running Variance .

J'espère que cela t'aides.

Remarque: j'ai fourni des liens vers tous les articles de blog et les formules mathématiques en latex (images) pour cette réponse. Mais, en raison de ma mauvaise réputation (< 10); Je suis limité à seulement 2 hyperliens et absolument aucune image. Pardon à propos de ça. J'espère que cela n'enlève rien au contenu.

22
Mike Taylor

J'ai traité le même problème.

La moyenne est simple à calculer de manière itérative, mais vous devez conserver l'historique complet des valeurs dans un tampon circulaire.

next_index = (index + 1) % window_size;    // oldest x value is at next_index, wrapping if necessary.

new_mean = mean + (x_new - xs[next_index])/window_size;

J'ai adapté l'algorithme de Welford et il fonctionne pour toutes les valeurs que j'ai testées.

varSum = var_sum + (x_new - mean) * (x_new - new_mean) - (xs[next_index] - mean) * (xs[next_index] - new_mean);

xs[next_index] = x_new;
index = next_index;

Pour obtenir la variance actuelle, il suffit de diviser varSum par la taille de la fenêtre: variance = varSum / window_size;

17
DanS

Si vous préférez le code aux mots (fortement basé sur le post de DanS): http://calcandstuff.blogspot.se/2014/02/rolling-variance-calculation.html

public IEnumerable RollingSampleVariance(IEnumerable data, int sampleSize)
{
    double mean = 0;
    double accVar = 0;

    int n = 0;
    var queue = new Queue(sampleSize);

    foreach(var observation in data)
    {
        queue.Enqueue(observation);
        if (n < sampleSize)
        {
            // Calculating first variance
            n++;
            double delta = observation - mean;
            mean += delta / n;
            accVar += delta * (observation - mean);
        }
        else
        {
            // Adjusting variance
            double then = queue.Dequeue();
            double prevMean = mean;
            mean += (observation - then) / sampleSize;
            accVar += (observation - prevMean) * (observation - mean) - (then - prevMean) * (then - mean);
        }

        if (n == sampleSize)
            yield return accVar / (sampleSize - 1);
    }
}
7
Joachim

Voici une approche de division et de conquête qui a O(log k)- time updates, où k est le nombre d'échantillons. Il devrait être relativement stable pour les mêmes raisons que la sommation par paire et les FFT sont stables, mais c'est un peu compliqué et la constante n'est pas géniale.

Supposons que nous ayons une séquence A de longueur m avec moyenne E(A) et variance V(A), et une séquence B de longueur n avec moyenne E(B) et variance V(B). Soit C la concaténation de A et B. On a

p = m / (m + n)
q = n / (m + n)
E(C) = p * E(A) + q * E(B)
V(C) = p * (V(A) + (E(A) + E(C)) * (E(A) - E(C))) + q * (V(B) + (E(B) + E(C)) * (E(B) - E(C)))

Farcissez maintenant les éléments dans un arbre rouge-noir, où chaque nœud est décoré avec la moyenne et la variance du sous-arbre enraciné sur ce nœud. Insérer à droite; supprimer à gauche. (Etant donné que nous n'accédons qu'aux extrémités, une arborescence splay peut-être être O(1) amorti, mais je suppose qu'amorti est un problème pour votre application.) Si k est connu au moment de la compilation, vous pouvez probablement dérouler la boucle intérieure de style FFTW.

5
userOVER9000

En fait, l’algorithme de Welford peut être facilement adapté au calcul de pondéré Variance . En définissant des pondérations sur -1, vous devriez pouvoir annuler efficacement les éléments. Je n'ai pas vérifié si cela permettait des poids négatifs, mais à première vue, cela devrait le faire!

J'ai effectué une petite expérience avec ELKI :

void testSlidingWindowVariance() {
MeanVariance mv = new MeanVariance(); // ELKI implementation of weighted Welford!
MeanVariance mc = new MeanVariance(); // Control.

Random r = new Random();
double[] data = new double[1000];
for (int i = 0; i < data.length; i++) {
  data[i] = r.nextDouble();
}

// Pre-roll:
for (int i = 0; i < 10; i++) {
  mv.put(data[i]);
}
// Compare to window approach
for (int i = 10; i < data.length; i++) {
  mv.put(data[i-10], -1.); // Remove
  mv.put(data[i]);
  mc.reset(); // Reset statistics
  for (int j = i - 9; j <= i; j++) {
    mc.put(data[j]);
  }
  assertEquals("Variance does not agree.", mv.getSampleVariance(),
    mc.getSampleVariance(), 1e-14);
}
}

J'obtiens environ 14 chiffres de précision par rapport à l'algorithme exact en deux passes; c'est à peu près tout ce que l'on peut attendre des doubles. Notez que Welford ne a un coût de calcul en raison des divisions supplémentaires - cela prend environ deux fois plus de temps que l'algorithme exact à deux passes. Si la taille de votre fenêtre est petite, il peut être beaucoup plus judicieux de recalculer la moyenne, puis en une seconde la variance toutes fois.

J'ai ajouté cette expérience en tant que test unitaire à ELKI, vous pouvez voir le code source complet ici: http://elki.dbs.ifi.lmu.de/browser/elki/trunk/test/de/lmu/ifi/dbs /elki/math/TestSlidingVariance.Java Il se compare également à la variance exacte à deux passes.

Cependant, sur des ensembles de données asymétriques, le comportement peut être différent. Cet ensemble de données est évidemment distribué uniformément; mais j'ai aussi essayé un tableau trié et cela a fonctionné.

4
Erich Schubert

Je sais que cette question est ancienne, mais au cas où quelqu'un d'autre serait intéressé, suivez le code python. Il est inspiré par johndcook blog, @ Joachim's, le code de @ DanS et les commentaires de @Jaime. Le code ci-dessous donne encore de petites imprécisions pour les petites tailles de fenêtres de données. Prendre plaisir.

from __future__ import division
import collections
import math


class RunningStats:
    def __init__(self, WIN_SIZE=20):
        self.n = 0
        self.mean = 0
        self.run_var = 0
        self.WIN_SIZE = WIN_SIZE

        self.windows = collections.deque(maxlen=WIN_SIZE)

    def clear(self):
        self.n = 0
        self.windows.clear()

    def Push(self, x):

        self.windows.append(x)

        if self.n <= self.WIN_SIZE:
            # Calculating first variance
            self.n += 1
            delta = x - self.mean
            self.mean += delta / self.n
            self.run_var += delta * (x - self.mean)
        else:
            # Adjusting variance
            x_removed = self.windows.popleft()
            old_m = self.mean
            self.mean += (x - x_removed) / self.WIN_SIZE
            self.run_var += (x + x_removed - old_m - self.mean) * (x - x_removed)

    def get_mean(self):
        return self.mean if self.n else 0.0

    def get_var(self):
        return self.run_var / (self.WIN_SIZE - 1) if self.n > 1 else 0.0

    def get_std(self):
        return math.sqrt(self.get_var())

    def get_all(self):
        return list(self.windows)

    def __str__(self):
        return "Current window values: {}".format(list(self.windows))
2
ewerlopes

Pour seulement 20 valeurs, il est trivial d’adapter la méthode exposée ici (je n’ai pas dit vite, cependant).

Vous pouvez simplement choisir un tableau de 20 classes RunningStat.

Les 20 premiers éléments du flux sont quelque peu spéciaux, mais une fois que cela est fait, c'est beaucoup plus simple:

  • lorsqu'un nouvel élément arrive, effacez l'instance actuelle RunningStat, ajoutez l'élément aux 20 instances et incrémentez le "compteur" (modulo 20) qui identifie la nouvelle instance "complète" RunningStat
  • à tout moment, vous pouvez consulter l'instance "complète" actuelle pour obtenir votre variante en cours d'exécution.

Vous remarquerez évidemment que cette approche n'est pas vraiment évolutive ...

Vous pouvez également noter qu'il y a un peu de redondance dans les nombres que nous conservons (si vous choisissez la classe RunningStat complète). Une amélioration évidente consisterait à conserver directement les 20 dernières Mk et Sk.

Je ne peux pas penser à une meilleure formule utilisant cet algorithme particulier, je crains que sa formulation récursive ne nous lie un peu.

1
Matthieu M.

Je suis impatient de me tromper sur ce point mais je ne pense pas que cela puisse être fait "rapidement". Cela dit, une grande partie du calcul consiste à garder une trace du VE par-dessus la fenêtre, ce qui peut être fait facilement. 

Je vais partir avec la question: êtes-vous sûr que vous avez besoin d'une fonction fenêtrée? Sauf si vous travaillez avec de très grandes fenêtres, il est probablement préférable d'utiliser un algorithme prédéfini bien connu. 

1
Andrew White

Voici une autre solution O(log k): trouve la séquence d'origine dans la séquence, puis additionnez les paires, puis quadruples, etc. pour obtenir votre réponse. Par exemple:

|||||||||||||||||||||||||  // Squares
| | | | | | | | | | | | |  // Sum of squares for pairs
|   |   |   |   |   |   |  // Pairs of pairs
|       |       |       |  // (etc.)
|               |
   ^------------------^    // Want these 20, which you can get with
        |       |          // one...
    |   |       |   |      // two, three...
                    | |    // four...
   ||                      // five stored values.

Maintenant, vous utilisez votre formule standard E (x ^ 2) -E (x) ^ 2 et vous avez terminé. (Pas si vous avez besoin d'une bonne stabilité pour de petits ensembles de nombres; cela supposait que c'était uniquement une accumulation d'erreur roulante qui posait problème)

Cela dit, la somme de 20 nombres carrés est très rapide de nos jours sur la plupart des architectures. Si vous en faisiez plus, disons quelques centaines, une méthode plus efficace serait clairement préférable. Mais je ne suis pas sûr que la force brute ne soit pas la solution.

1
Rex Kerr

Je suppose que garder trace de vos 20 échantillons, Sum (X ^ 2 à partir de 1..20) et Sum (X à partir de 1..20), puis recalculer successivement les deux sommes à chaque itération n’est pas assez efficace? Il est possible de recalculer la nouvelle variance sans additionner, équerrer, etc., tous les échantillons à chaque fois.

Un péché:

Sum(X^2 from 2..21) = Sum(X^2 from 1..20) - X_1^2 + X_21^2
Sum(X from 2..21) = Sum(X from 1..20) - X_1 + X_21
1
John

Il ne s'agit que d'un ajout mineur à l'excellente réponse fournie par DanS. Les équations suivantes servent à retirer l’échantillon le plus ancien de la fenêtre et à mettre à jour la moyenne et la variance. Ceci est utile, par exemple, si vous voulez utiliser des fenêtres plus petites près du bord droit de votre flux de données d’entrée (c’est-à-dire supprimer simplement l’échantillon de fenêtre le plus ancien sans en ajouter un nouvel).

window_size -= 1; % decrease window size by 1 sample
new_mean = prev_mean + (prev_mean - x_old) / window_size
varSum = varSum - (prev_mean - x_old) * (new_mean - x_old)

Ici, x_old est l'échantillon le plus ancien de la fenêtre que vous souhaitez supprimer.

0
vibe