Как эффективно рассчитать текущее стандартное отклонение?


87

У меня есть массив списков чисел, например:

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

Я хотел бы эффективно вычислить среднее значение и стандартное отклонение по каждому индексу списка по всем элементам массива.

В среднем я просматривал массив и суммировал значение по заданному индексу списка. В конце я делю каждое значение в моем «среднем списке» на n(я работаю с генеральной совокупностью, а не с выборкой из совокупности).

Чтобы вычислить стандартное отклонение, я снова прохожу через цикл, теперь, когда у меня вычислено среднее значение.

Я хотел бы избежать повторения массива дважды: один раз для среднего, а затем один раз для SD (после того, как у меня будет среднее значение).

Есть ли эффективный метод вычисления обоих значений, только проходя через массив один раз? Подойдет любой код на интерпретируемом языке (например, Perl или Python) или псевдокоде.


7
Другой язык, но тот же алгоритм: stackoverflow.com/questions/895929/…
dmckee --- котенок экс-модератора

Спасибо, проверю этот алгоритм. Похоже на то, что мне нужно.
Alex Reynolds

Спасибо, что указали мне правильный ответ, dmckee. Я хотел бы поставить вам галочку "лучший ответ", если вы хотите уделить время и добавить свой ответ ниже (если вам нужны баллы).
Alex Reynolds

1
Кроме того, есть несколько примеров на rosettacode.org/wiki/Standard_Deviation
Гленн Джекман,

1
В Википедии есть реализация Python en.wikipedia.org/wiki/…
Хэмиш Грубиджан,

Ответы:


116

Ответ - использовать алгоритм Велфорда, который очень четко определен после «наивных методов» в:

Он более стабилен в числовом отношении, чем двухпроходный или интерактивный сборщик простой суммы квадратов, предложенный в других ответах. Стабильность действительно имеет значение только тогда, когда у вас есть много значений, которые близки друг к другу, поскольку они приводят к так называемой « катастрофической отмене » в литературе с плавающей запятой.

Вы также можете освежить в памяти разницу между делением на количество выборок (N) и N-1 при расчете дисперсии (квадратичное отклонение). Деление на N-1 приводит к объективной оценке дисперсии по выборке, тогда как деление на N в среднем недооценивает дисперсию (поскольку не принимает во внимание дисперсию между средним по выборке и истинным средним).

Я написал две записи в блоге по этой теме, в которых подробно рассказывается, в том числе о том, как удалить предыдущие значения в Интернете:

Вы также можете взглянуть на мою реализацию Java; документация javadoc, исходный код и модульные тесты доступны онлайн:


1
+1, за то, что позаботился об удалении значений из алгоритма
Велфорда

3
Хороший ответ, +1 за напоминание читателю о разнице между стандартным stddev населения и образцом stddev.
Assad Ebrahim

Вернувшись к этому вопросу после всех этих лет, я просто хотел сказать слова благодарности за то, что нашел время дать отличный ответ.
Alex Reynolds

76

Основной ответ - накапливать сумму как x (назовите это «сумма_x1»), так и x 2 (назовите это «сумма_x2») по мере продвижения. Тогда значение стандартного отклонения:

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

где

mean = sum_x / n

Это стандартное отклонение выборки; вы получите стандартное отклонение совокупности, используя в качестве делителя «n» вместо «n - 1».

Возможно, вам придется побеспокоиться о числовой стабильности измерения разницы между двумя большими числами, если вы имеете дело с большими выборками. Перейдите к внешним ссылкам в других ответах (Википедия и т. Д.) Для получения дополнительной информации.


Это то, что я собирался предложить. Это лучший и самый быстрый способ, при условии, что ошибки точности не являются проблемой.
Рэй Хидаят,

2
Я решил использовать алгоритм Велфорда, поскольку он работает более надежно с теми же вычислительными затратами.
Alex Reynolds

2
Это упрощенная версия ответа, которая может давать нереальные результаты в зависимости от ввода (например, когда sum_x2 <sum_x1 * sum_x1). Чтобы гарантировать действительный реальный результат, используйте `sd = sqrt (((n * sum_x2) - (sum_x1 * sum_x1)) / (n * (n - 1)))
Dan Tao

2
@Dan указывает на действительную проблему - приведенная выше формула не работает для x> 1, потому что вы в конечном итоге берете sqrt отрицательного числа. Подход Кнута: sqrt ((sum_x2 / n) - (mean * mean)), где mean = (sum_x / n).
G__

1
@UriLoya - вы ничего не сказали о том, как вы рассчитываете значения. Однако, если вы используете intв C для хранения суммы квадратов, вы столкнетесь с проблемами переполнения со значениями, которые вы перечисляете.
Джонатан Леффлер,

38

Вот дословный перевод реализации алгоритма Велфорда на чистый Python с 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())

Применение:

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
Это должен быть принятый ответ, поскольку это единственный правильный ответ, который показывает алгоритм со ссылкой на Knuth.
Йохан Лундберг

26

Возможно, это не то, о чем вы спрашивали, но ... Если вы используете массив numpy, он будет работать за вас эффективно:

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 ]

Кстати, в этом сообщении блога есть интересное обсуждение и комментарии к однопроходным методам вычисления средств и отклонений:


14

Модуль Python runstats предназначен именно для этого. Установите runstats из PyPI:

pip install runstats

Сводки Runstats могут производить среднее значение, дисперсию, стандартное отклонение, асимметрию и эксцесс за один проход данных. Мы можем использовать это для создания вашей «работающей» версии.

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

Сводные статистические данные основаны на методе Кнута и Велфорда для вычисления стандартного отклонения за один проход, как описано в «Искусство компьютерного программирования», том 2, стр. 232, 3-е издание. Преимущество этого - числовая стабильность и точность результатов.

Отказ от ответственности: я являюсь автором модуля Python runstats.


Хороший модуль. Было бы интересно, если бы был метод Statisticshas для .popрасчета скользящей статистики.
Густаво Безерра

@GustavoBezerra runstatsне поддерживает внутренний список значений, поэтому я не уверен, что это возможно. Но запросы на включение приветствуются.
GrantJ 08

8

Статистика :: Descriptive - очень приличный модуль Perl для таких вычислений:

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

Выход:

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

Взгляните на PDL (произносится как «пиддл!»).

Это язык данных Perl, разработанный для высокоточной математики и научных вычислений.

Вот пример с вашими цифрами ....

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;


Что производит:

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]


Взгляните на PDL :: Primitive для получения дополнительной информации о статусе. функции . Похоже, это наводит на мысль, что ADEV - это «стандартное отклонение».

Однако это может быть PRMS (что показано в примере Sinan Statistics :: Descriptive) или RMS (что показано в примере Ars NumPy). Думаю, один из этих трех должен быть прав ;-)

Для получения дополнительной информации о PDL посмотрите:


1
Это не текущий расчет.
Джейк

3

Насколько велик ваш массив? Если это не миллионы элементов, не беспокойтесь о повторении цикла дважды. Код прост и легко тестируется.

Я бы предпочел использовать расширение numpy array maths для преобразования вашего массива массивов в numpy 2D-массив и напрямую получить стандартное отклонение:

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

Если это не вариант и вам нужно решение на чистом Python, продолжайте читать ...

Если ваш массив

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

Тогда стандартное отклонение:

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

Если вы настроены пройти через массив только один раз, текущие суммы можно объединить.

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

Это не так элегантно, как решение для понимания списка выше.


Мне действительно приходится иметь дело с миллиардами цифр, что и мотивирует мою потребность в эффективном решении. Благодарность!
Alex Reynolds

Дело не в том, насколько велик набор данных, а в том, как ЧАСТО Я должен выполнять 3500 различных вычислений стандартного отклонения более 500 элементов для каждого вычисления в секунду
PirateApp

1

Вы можете посмотреть статью в Википедии о стандартном отклонении , в частности, раздел о методах быстрого расчета.

Я также нашел статью, в которой используется Python, вы сможете использовать код в ней без особых изменений: Подсознательные сообщения - выполнение стандартных отклонений .


Версия Subliminal Messages не очень стабильна численно.
Dave


1

Вот "однострочный", разбросанный по нескольким строкам, в стиле функционального программирования:

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

Как описывает следующий ответ: Предоставляет ли pandas / scipy / numpy функцию совокупного стандартного отклонения? Модуль Python Pandas содержит метод вычисления текущего или совокупного стандартного отклонения . Для этого вам нужно будет преобразовать ваши данные в фреймворк pandas (или серию, если это 1D), но для этого есть функции.


1

Мне нравится выражать обновление так:

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)

так, чтобы однопроходная функция выглядела так:

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)

обратите внимание, что здесь вычисляется дисперсия выборки (1 / N), а не несмещенная оценка дисперсии генеральной совокупности (которая использует коэффициент нормализации 1 / (N-1)). В отличие от других ответов, переменная var, отслеживающая текущую дисперсию, не растет пропорционально количеству выборок. Всегда это просто дисперсия набора выборок, наблюдаемых до сих пор (нет окончательного «деления на n» для получения дисперсии).

В классе это будет выглядеть так:

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

Это также работает для взвешенных образцов:

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

Вот практический пример того, как можно реализовать текущее стандартное отклонение с помощью python и numpy:

a = np.arange(1, 10)
s = 0
s2 = 0
for i in range(0, len(a)):
    s += a[i]
    s2 += a[i] ** 2 
    n = (i + 1)
    m = s / n
    std = np.sqrt((s2 / n) - (m * m))
    print(std, np.std(a[:i + 1]))

Это распечатает рассчитанное стандартное отклонение и стандартное отклонение проверки, рассчитанное с помощью numpy:

0.0 0.0
0.5 0.5
0.8164965809277263 0.816496580927726
1.118033988749895 1.118033988749895
1.4142135623730951 1.4142135623730951
1.707825127659933 1.707825127659933
2.0 2.0
2.29128784747792 2.29128784747792
2.5819888974716116 2.581988897471611

Я просто использую формулу, описанную в этой теме:

stdev = sqrt((sum_x2 / n) - (mean * mean)) 
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.