web-dev-qa-db-fra.com

Comment calculer efficacement un écart-type en cours?

J'ai un tableau de listes de nombres, par exemple:

[0] (0.01, 0.01, 0.02, 0.04, 0.03)
[1] (0.00, 0.02, 0.02, 0.03, 0.02)
[2] (0.01, 0.02, 0.02, 0.03, 0.02)
     ...
[n] (0.01, 0.00, 0.01, 0.05, 0.03)

Ce que je voudrais faire, c'est calculer efficacement l'écart moyen et l'écart type à chaque index d'une liste, pour tous les éléments du tableau.

Pour faire la moyenne, j'ai parcouru le tableau en faisant la somme de la valeur à un index donné d'une liste. À la fin, je divise chaque valeur de ma "liste des moyennes" par n.

Pour faire l’écart-type, j’ai parcouru de nouveau, maintenant que la moyenne est calculée.

Je voudrais éviter de parcourir le tableau deux fois, une fois pour la moyenne et une fois pour le SD (après avoir une moyenne). 

Existe-t-il une méthode efficace pour calculer les deux valeurs, en parcourant le tableau une seule fois? N'importe quel code dans un langage interprété (par exemple Perl ou Python) ou un pseudocode convient.

74
Alex Reynolds

La solution consiste à utiliser l'algorithme de Welford, qui est très clairement défini après les "méthodes naïves" dans:

Il est plus stable numériquement que la simple somme de carrés de capteurs en deux passes ou en ligne suggérée dans d'autres réponses. La stabilité ne compte vraiment que lorsque vous avez beaucoup de valeurs proches les unes des autres car elles mènent à ce que l’on appelle " annulation catastrophique " dans la littérature en virgule flottante.

Vous voudrez peut-être aussi préciser la différence entre la division par le nombre d'échantillons (N) et N-1 dans le calcul de la variance (écart au carré). La division par N-1 donne une estimation non biaisée de la variance à partir de l'échantillon, tandis que la division par N en moyenne sous-estime la variance (car elle ne prend pas en compte la variance entre la moyenne de l'échantillon et la moyenne vraie).

J'ai écrit deux entrées de blog sur le sujet qui vont dans plus de détails, y compris comment supprimer les valeurs précédentes en ligne:

Vous pouvez également consulter mon implémentation Java. les tests javadoc, source et unitaire sont tous en ligne:

101
Bob Carpenter

La réponse de base consiste à accumuler la somme de x (appelez-le 'sum_x1') et de x 2 (appelez-le 'sum_x2') au fur et à mesure. La valeur de l'écart type est alors:

stdev = sqrt((sum_x2 / n) - (mean * mean)) 

mean = sum_x / n

Ceci est l’écart type de l’échantillon; vous obtenez l'écart type de la population en utilisant 'n' au lieu de 'n - 1' comme diviseur.

Vous devrez peut-être vous inquiéter de la stabilité numérique de la différence entre deux grands nombres si vous avez affaire à de grands échantillons. Allez aux références externes dans d'autres réponses (Wikipedia, etc.) pour plus d'informations.

69
Jonathan Leffler

Peut-être pas ce que vous demandiez, mais ... Si vous utilisez un tableau numpy, il fera le travail pour vous, efficacement:

from numpy import array

nums = array(((0.01, 0.01, 0.02, 0.04, 0.03),
              (0.00, 0.02, 0.02, 0.03, 0.02),
              (0.01, 0.02, 0.02, 0.03, 0.02),
              (0.01, 0.00, 0.01, 0.05, 0.03)))

print nums.std(axis=1)
# [ 0.0116619   0.00979796  0.00632456  0.01788854]

print nums.mean(axis=1)
# [ 0.022  0.018  0.02   0.02 ]

À propos, il y a des discussions intéressantes dans cet article de blog et des commentaires sur les méthodes à passe unique pour les moyennes et les écarts de calcul:

26
ars

Voici une traduction littérale en pur Python de l’implémentation de l’algorithme de Welford de http://www.johndcook.com/standard_deviation.html :

https://github.com/liyanage/python-modules/blob/master/running_stats.py

class RunningStats:

    def __init__(self):
        self.n = 0
        self.old_m = 0
        self.new_m = 0
        self.old_s = 0
        self.new_s = 0

    def clear(self):
        self.n = 0

    def Push(self, x):
        self.n += 1

        if self.n == 1:
            self.old_m = self.new_m = x
            self.old_s = 0
        else:
            self.new_m = self.old_m + (x - self.old_m) / self.n
            self.new_s = self.old_s + (x - self.old_m) * (x - self.new_m)

            self.old_m = self.new_m
            self.old_s = self.new_s

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

    def variance(self):
        return self.new_s / (self.n - 1) if self.n > 1 else 0.0

    def standard_deviation(self):
        return math.sqrt(self.variance())

Usage:

rs = RunningStats()
rs.Push(17.0);
rs.Push(19.0);
rs.Push(24.0);

mean = rs.mean();
variance = rs.variance();
stdev = rs.standard_deviation();
23
Marc Liyanage

Le module Python runstats est juste pour ce genre de chose. Installez runstats depuis PyPI:

pip install runstats

Les résumés Runstats peuvent produire la moyenne, la variance, l'écart type, l'asymétrie et le kurtosis en une seule passe de données. Nous pouvons utiliser cela pour créer votre version "courante".

from runstats import Statistics

stats = [Statistics() for num in range(len(data[0]))]

for row in data:

    for index, val in enumerate(row):
        stats[index].Push(val)

    for index, stat in enumerate(stats):
        print 'Index', index, 'mean:', stat.mean()
        print 'Index', index, 'standard deviation:', stat.stddev()

Les résumés statistiques sont basés sur la méthode de Knuth et Welford pour calculer l'écart type en une passe, comme décrit dans Art of Computer Programming, Vol. 232, 3ème édition. L'avantage de ceci est des résultats numériquement stables et précis.

Disclaimer: Je suis l'auteur du module runstats Python.

10
GrantJ

Jetez un oeil à _ PDL (prononcé «piddle!»). 

C'est le langage de données Perl conçu pour les mathématiques de haute précision et l'informatique scientifique.

Voici un exemple utilisant vos chiffres ....

use strict;
use warnings;
use PDL;

my $figs = pdl [
    [0.01, 0.01, 0.02, 0.04, 0.03],
    [0.00, 0.02, 0.02, 0.03, 0.02],
    [0.01, 0.02, 0.02, 0.03, 0.02],
    [0.01, 0.00, 0.01, 0.05, 0.03],
];

my ( $mean, $prms, $median, $min, $max, $adev, $rms ) = statsover( $figs );

say "Mean scores:     ", $mean;
say "Std dev? (adev): ", $adev;
say "Std dev? (prms): ", $prms;
say "Std dev? (rms):  ", $rms;


Qui produit:

Mean scores:     [0.022 0.018 0.02 0.02]
Std dev? (adev): [0.0104 0.0072 0.004 0.016]
Std dev? (prms): [0.013038405 0.010954451 0.0070710678 0.02]
Std dev? (rms):  [0.011661904 0.009797959 0.0063245553 0.017888544]


Regardez PDL :: Primitive pour plus d’informations sur le statsover une fonction. Cela semble suggérer qu'ADEV est "l'écart type". 

Cependant, il peut s'agir de PRMS (comme le montre l'exemple de Statistics :: Descriptive de Sinan) ou RMS (comme le montre l'exemple de NumPy d'ars). Je suppose que l'un de ces trois doit avoir raison ;-)

Pour plus d’informations PDL, consultez:

8
draegtun

Statistics :: Descriptive est un module Perl très décent pour ces types de calcul:

#!/usr/bin/Perl

use strict; use warnings;

use Statistics::Descriptive qw( :all );

my $data = [
    [ 0.01, 0.01, 0.02, 0.04, 0.03 ],
    [ 0.00, 0.02, 0.02, 0.03, 0.02 ],
    [ 0.01, 0.02, 0.02, 0.03, 0.02 ],
    [ 0.01, 0.00, 0.01, 0.05, 0.03 ],
];

my $stat = Statistics::Descriptive::Full->new;
# You also have the option of using sparse data structures

for my $ref ( @$data ) {
    $stat->add_data( @$ref );
    printf "Running mean: %f\n", $stat->mean;
    printf "Running stdev: %f\n", $stat->standard_deviation;
}
__END__

Sortie:

C:\Temp> g
Running mean: 0.022000
Running stdev: 0.013038
Running mean: 0.020000
Running stdev: 0.011547
Running mean: 0.020000
Running stdev: 0.010000
Running mean: 0.020000
Running stdev: 0.012566
7
Sinan Ünür

Quelle est la taille de votre tableau? À moins que des millions d'éléments ne soient longs, ne vous inquiétez pas de le lire en boucle deux fois. Le code est simple et facile à tester.

Ma préférence serait d’utiliser l’extension mathématique numpy array pour convertir votre tableau de tableaux en tableau numérique Numpy et obtenir directement l’écart type:

>>> x = [ [ 1, 2, 4, 3, 4, 5 ], [ 3, 4, 5, 6, 7, 8 ] ] * 10
>>> import numpy
>>> a = numpy.array(x)
>>> a.std(axis=0) 
array([ 1. ,  1. ,  0.5,  1.5,  1.5,  1.5])
>>> a.mean(axis=0)
array([ 2. ,  3. ,  4.5,  4.5,  5.5,  6.5])

Si ce n'est pas une option et que vous avez besoin d'une solution Python pure, continuez à lire ...

Si votre tableau est 

x = [ 
      [ 1, 2, 4, 3, 4, 5 ],
      [ 3, 4, 5, 6, 7, 8 ],
      ....
]

Alors l'écart-type est:

d = len(x[0])
n = len(x)
sum_x = [ sum(v[i] for v in x) for i in range(d) ]
sum_x2 = [ sum(v[i]**2 for v in x) for i in range(d) ]
std_dev = [ sqrt((sx2 - sx**2)/N)  for sx, sx2 in Zip(sum_x, sum_x2) ]

Si vous êtes déterminé à parcourir votre tableau une seule fois, vous pouvez combiner les sommes en cours.

sum_x  = [ 0 ] * d
sum_x2 = [ 0 ] * d
for v in x:
   for i, t in enumerate(v):
   sum_x[i] += t
   sum_x2[i] += t**2

Ce n'est pas aussi élégant que la solution de compréhension de liste ci-dessus.

3
Stephen Simmons

Je pense que cette question va vous aider. Déviation standard

2
peterdemin
n=int(raw_input("Enter no. of terms:"))

L=[]

for i in range (1,n+1):

    x=float(raw_input("Enter term:"))

    L.append(x)

sum=0

for i in range(n):

    sum=sum+L[i]

avg=sum/n

sumdev=0

for j in range(n):

    sumdev=sumdev+(L[j]-avg)**2

dev=(sumdev/n)**0.5

print "Standard deviation is", dev
1
Anuraag

Vous pouvez consulter l'article de Wikipedia sur Écart-type , en particulier la section sur les méthodes de calcul rapide.

Il y a aussi un article que j'ai trouvé qui utilise Python, vous devriez pouvoir utiliser le code qu'il contient sans trop de changement: Subliminal Messages - Running Standard déviations .

Comme le décrit la réponse suivante: pandas/scipy/numpy assure-t-il une fonction de déviation standard cumulative? Le module Python Pandas contient une méthode pour calculer l'écart type en cours ou cumulatif . Pour cela, vous devrez convertir vos données en un cadre de données pandas (ou une série s'il s'agit de 1D ), mais il existe des fonctions pour cela. 

1
Ramon Crehuet

Voici un "one-liner", réparti sur plusieurs lignes, dans un style de programmation fonctionnel:

def variance(data, opt=0):
    return (lambda (m2, i, _): m2 / (opt + i - 1))(
        reduce(
            lambda (m2, i, avg), x:
            (
                m2 + (x - avg) ** 2 * i / (i + 1),
                i + 1,
                avg + (x - avg) / (i + 1)
            ),
            data,
            (0, 0, 0)))
0
Mehrdad

J'aime exprimer la mise à jour de cette façon:

def running_update(x, N, mu, var):
    '''
        @arg x: the current data sample
        @arg N : the number of previous samples
        @arg mu: the mean of the previous samples
        @arg var : the variance over the previous samples
        @retval (N+1, mu', var') -- updated mean, variance and count
    '''
    N = N + 1
    rho = 1.0/N
    d = x - mu
    mu += rho*d
    var += rho*((1-rho)*d**2 - var)
    return (N, mu, var)

de sorte qu'une fonction en un seul passage ressemble à ceci:

def one_pass(data):
    N = 0
    mu = 0.0
    var = 0.0
    for x in data:
        N = N + 1
        rho = 1.0/N
        d = x - mu
        mu += rho*d
        var += rho*((1-rho)*d**2 - var)
        # could yield here if you want partial results
   return (N, mu, var)

notez qu'il s'agit du calcul de la variance de l'échantillon (1/N) et non de l'estimation non biaisée de la variance de la population (qui utilise un facteur de normalisation 1/(N-1)). Contrairement aux autres réponses, la variable, var, qui permet de suivre la variance en cours, ne croît pas proportionnellement au nombre d'échantillons. À tout moment, il s’agit simplement de la variance de l’ensemble des échantillons vus jusqu’à présent (il n’ya pas de "division par n" finale pour obtenir la variance).

Dans une classe, cela ressemblerait à ceci:

class RunningMeanVar(object):
    def __init__(self):
        self.N = 0
        self.mu = 0.0
        self.var = 0.0
    def Push(self, x):
        self.N = self.N + 1
        rho = 1.0/N
        d = x-self.mu
        self.mu += rho*d
        self.var += + rho*((1-rho)*d**2-self.var)
    # reset, accessors etc. can be setup as you see fit

Cela fonctionne également pour les échantillons pondérés:

def running_update(w, x, N, mu, var):
    '''
        @arg w: the weight of the current sample
        @arg x: the current data sample
        @arg mu: the mean of the previous N sample
        @arg var : the variance over the previous N samples
        @arg N : the number of previous samples
        @retval (N+w, mu', var') -- updated mean, variance and count
    '''
    N = N + w
    rho = w/N
    d = x - mu
    mu += rho*d
    var += rho*((1-rho)*d**2 - var)
    return (N, mu, var)
0
Dave