Wie kann eine laufende Standardabweichung effizient berechnet werden?


87

Ich habe eine Reihe von Listen mit Zahlen, z.

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

Ich möchte den Mittelwert und die Standardabweichung an jedem Index einer Liste über alle Array-Elemente hinweg effizient berechnen.

Um den Mittelwert zu ermitteln, habe ich das Array durchlaufen und den Wert an einem bestimmten Index einer Liste summiert. Am Ende teile ich jeden Wert in meiner "Durchschnittsliste" durch n(ich arbeite mit einer Population, nicht mit einer Stichprobe aus der Population).

Um die Standardabweichung zu machen, durchlaufe ich erneut, nachdem ich den Mittelwert berechnet habe.

Ich möchte vermeiden, das Array zweimal durchzugehen, einmal für den Mittelwert und dann einmal für die SD (nachdem ich einen Mittelwert habe).

Gibt es eine effiziente Methode zur Berechnung beider Werte, bei der das Array nur einmal durchlaufen wird? Jeder Code in einer interpretierten Sprache (z. B. Perl oder Python) oder Pseudocode ist in Ordnung.



Danke, ich werde diesen Algorithmus überprüfen. Klingt nach dem, was ich brauche.
Alex Reynolds

Danke, dass du mich auf die richtige Antwort hingewiesen hast, dmckee. Ich möchte Ihnen das Häkchen "Beste Antwort" geben, wenn Sie sich einen Moment Zeit nehmen möchten, um Ihre Antwort unten hinzuzufügen (wenn Sie die Punkte möchten).
Alex Reynolds

1
Es gibt auch einige Beispiele unter rosettacode.org/wiki/Standard_Deviation
Glenn Jackman

1
Wikipedia hat eine Python-Implementierung en.wikipedia.org/wiki/…
Hamish Grubijan

Antworten:


114

Die Antwort ist die Verwendung des Welford-Algorithmus, der nach den "naiven Methoden" in:

Es ist numerisch stabiler als die in anderen Antworten vorgeschlagene einfache Summe von Quadratsammlern mit zwei Durchgängen oder online. Die Stabilität ist nur dann wirklich wichtig, wenn Sie viele Werte haben, die nahe beieinander liegen, da sie in der Gleitkomma-Literatur zu einer sogenannten " katastrophalen Löschung " führen.

Möglicherweise möchten Sie auch den Unterschied zwischen der Division durch die Anzahl der Stichproben (N) und N-1 in der Varianzberechnung (quadratische Abweichung) auffrischen. Die Division durch N-1 führt zu einer unvoreingenommenen Schätzung der Varianz aus der Stichprobe, während die Division durch N im Durchschnitt die Varianz unterschätzt (da die Varianz zwischen dem Stichprobenmittelwert und dem wahren Mittelwert nicht berücksichtigt wird).

Ich habe zwei Blogeinträge zu diesem Thema geschrieben, in denen weitere Details behandelt werden, darunter das Löschen früherer Werte online:

Sie können sich auch mein Java-Gerät ansehen. Die Javadoc-, Quell- und Unit-Tests sind alle online:


1
+1, für das Löschen von Werten aus dem Welford-Algorithmus
Svisstack

3
Schöne Antwort, +1, um den Leser an den Unterschied zwischen einer Population stddev und einer Stichprobe stddev zu erinnern.
Assad Ebrahim

Nachdem ich nach all den Jahren auf diese Frage zurückgekommen war, wollte ich mich nur dafür bedanken, dass ich mir die Zeit genommen habe, eine großartige Antwort zu geben.
Alex Reynolds

75

Die grundlegende Antwort besteht darin, die Summe von x (nennen Sie es 'sum_x1') und x 2 (nennen Sie es 'sum_x2') zu akkumulieren, während Sie gehen. Der Wert der Standardabweichung ist dann:

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

wo

mean = sum_x / n

Dies ist die Standardabweichung der Stichprobe. Sie erhalten die Populationsstandardabweichung mit 'n' anstelle von 'n - 1' als Divisor.

Möglicherweise müssen Sie sich über die numerische Stabilität der Differenz zwischen zwei großen Zahlen Gedanken machen, wenn Sie mit großen Stichproben arbeiten. Weitere Informationen finden Sie in den externen Referenzen in anderen Antworten (Wikipedia usw.).


Das wollte ich vorschlagen. Dies ist der beste und schnellste Weg, vorausgesetzt, Präzisionsfehler sind kein Problem.
Ray Hidayat

2
Ich habe mich für den Welford-Algorithmus entschieden, da er bei gleichem Rechenaufwand zuverlässiger arbeitet.
Alex Reynolds

2
Dies ist eine vereinfachte Version der Antwort und kann abhängig von der Eingabe zu nicht realen Ergebnissen führen (dh wenn sum_x2 <sum_x1 * sum_x1). Um ein gültiges reales Ergebnis sicherzustellen, gehen Sie mit `sd = sqrt (((n * sum_x2) - (sum_x1 * sum_x1)) / (n * (n - 1)))
Dan Tao

2
@Dan weist auf ein gültiges Problem hin - die obige Formel bricht für x> 1 zusammen, weil Sie am Ende das Quadrat einer negativen Zahl nehmen. Der Knuth-Ansatz lautet: sqrt ((sum_x2 / n) - (Mittelwert * Mittelwert)) wobei Mittelwert = (sum_x / n).
G__

1
@UriLoya - Sie haben nichts darüber gesagt, wie Sie die Werte berechnen. Wenn Sie jedoch intin C die Summe der Quadrate speichern, treten bei den von Ihnen aufgelisteten Werten Überlaufprobleme auf.
Jonathan Leffler

36

Hier ist eine wörtliche reine Python-Übersetzung der Implementierung des Welford-Algorithmus von http://www.johndcook.com/standard_deviation.html :

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

import math

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

Verwendung:

rs = RunningStats()
rs.push(17.0)
rs.push(19.0)
rs.push(24.0)

mean = rs.mean()
variance = rs.variance()
stdev = rs.standard_deviation()

print(f'Mean: {mean}, Variance: {variance}, Std. Dev.: {stdev}')

9
Dies sollte die akzeptierte Antwort sein, da dies die einzige ist, die sowohl korrekt ist als auch den Algorithmus in Bezug auf Knuth zeigt.
Johan Lundberg

26

Vielleicht nicht das, was Sie gefragt haben, aber ... Wenn Sie ein numpy-Array verwenden, erledigt es die Arbeit effizient für Sie:

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 ]

Übrigens gibt es in diesem Blog-Beitrag einige interessante Diskussionen und Kommentare zu One-Pass-Methoden zur Berechnung von Mitteln und Abweichungen:


14

Das Python-Runstats-Modul ist genau für diese Art von Dingen gedacht . Installieren Sie Runstats von PyPI:

pip install runstats

Runstats-Zusammenfassungen können den Mittelwert, die Varianz, die Standardabweichung, die Schiefe und die Kurtosis in einem einzigen Datenlauf erzeugen. Wir können dies verwenden, um Ihre "laufende" Version zu erstellen.

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

Statistikzusammenfassungen basieren auf der Knuth- und Welford-Methode zur Berechnung der Standardabweichung in einem Durchgang, wie in Art of Computer Programming, Vol. 2, p. 232, 3. Auflage. Der Vorteil davon sind numerisch stabile und genaue Ergebnisse.

Haftungsausschluss: Ich bin der Autor des Python-Runstats-Moduls.


Schönes Modul. Es wäre interessant, wenn es Statisticseine .popMethode gäbe , mit der auch fortlaufende Statistiken berechnet werden könnten.
Gustavo Bezerra

@GustavoBezerra runstatsführt keine interne Werteliste , daher bin ich mir nicht sicher, ob dies möglich ist. Pull-Anfragen sind jedoch willkommen.
GrantJ

8

Statistics :: Descriptive ist ein sehr anständiges Perl-Modul für diese Art von Berechnungen:

#!/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__

Ausgabe:

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

8

Schauen Sie sich PDL an (ausgesprochen "Piddle!").

Dies ist die Perl-Datensprache, die für hochpräzise Mathematik und wissenschaftliches Rechnen entwickelt wurde.

Hier ist ein Beispiel mit Ihren Zahlen ....

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;


Welches produziert:

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]


Werfen Sie einen Blick auf PDL :: Primitive für weitere Informationen über die statsover Funktion. Dies scheint darauf hinzudeuten, dass ADEV die "Standardabweichung" ist.

Es kann sich jedoch um PRMS (das Sinans Statistics :: Descriptive-Beispiel zeigt) oder RMS (das NsPy-Beispiel von ars zeigt) handeln. Ich denke einer dieser drei muss stimmen ;-)

Weitere PDL-Informationen finden Sie unter:


1
Dies ist keine laufende Berechnung.
Jake

3

Wie groß ist dein Array? Machen Sie sich keine Sorgen, wenn Sie nicht zig Millionen Elemente lang sind. Der Code ist einfach und leicht zu testen.

Ich würde es vorziehen, die mathematische Erweiterung des numpy- Arrays zu verwenden, um Ihr Array von Arrays in ein numpy-2D-Array zu konvertieren und die Standardabweichung direkt zu erhalten:

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

Wenn dies keine Option ist und Sie eine reine Python-Lösung benötigen, lesen Sie weiter ...

Wenn Ihr Array ist

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

Dann ist die Standardabweichung:

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

Wenn Sie entschlossen sind, Ihr Array nur einmal zu durchlaufen, können die laufenden Summen kombiniert werden.

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

Dies ist bei weitem nicht so elegant wie die oben beschriebene Lösung zum Listenverständnis.


Ich muss mich tatsächlich mit Millionen von Zahlen auseinandersetzen, was mein Bedürfnis nach einer effizienten Lösung motiviert. Vielen Dank!
Alex Reynolds

Es geht nicht darum, wie groß der Datensatz ist, es geht darum, wie oft ich 3500 verschiedene Standardabweichungsberechnungen über 500 Elemente pro Berechnung pro Sekunde durchführen muss
PirateApp


1

Ich denke, dieses Problem wird Ihnen helfen. Standardabweichung


+1 @Lasse V. Karlsens Link zu Wikipedia ist gut, aber dies ist der richtige Algorithmus, den ich verwendet habe ...
kenny

1

Hier ist ein "Einzeiler", der über mehrere Zeilen verteilt ist und einen funktionalen Programmierstil aufweist:

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

1
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

Ich möchte das Update folgendermaßen ausdrücken:

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)

so dass eine One-Pass-Funktion folgendermaßen aussehen würde:

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)

Beachten Sie, dass dies die Stichprobenvarianz (1 / N) berechnet, nicht die unvoreingenommene Schätzung der Populationsvarianz (die einen 1 / (N-1) -Normalisierungsfaktor verwendet). Im Gegensatz zu den anderen Antworten varwächst die Variable, die die laufende Varianz verfolgt, nicht proportional zur Anzahl der Stichproben. Zu allen Zeiten ist es nur die Varianz des bisher gesehenen Satzes von Stichproben (es gibt keine endgültige "Division durch n", um die Varianz zu erhalten).

In einer Klasse würde es so aussehen:

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

Dies funktioniert auch für gewichtete Proben:

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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.