滚动方差算法

时间:2011-02-28 20:46:23

标签: algorithm statistics variance

我正在尝试找到一种有效的,数值稳定的算法来计算滚动方差(例如,20周期滚动窗口的方差)。我知道Welford algorithm有效地计算了数字流的运行方差(它只需要一次通过),但我不确定这是否可以适应滚动窗口。我还希望解决方案避免John D. Cook在this article顶部讨论的准确性问题。任何语言的解决方案都可以。

11 个答案:

答案 0 :(得分:22)

我也遇到过这个问题。在计算运行累积方差方面有一些很棒的帖子,例如John Cooke的准确计算运行方差帖子和数字探索的帖子,用于计算样本和人口差异的Python代码,协方差和相关系数。只是找不到任何适合滚动窗口的东西。

Subluminal Messages的Running Standard Deviations帖子对于使滚动窗口公式起作用至关重要。 Jim使用值的平方差的幂和与Welford使用均值的平方差之和的方法。公式如下:

  

PSA今天= PSA(昨天)+(((今天x今天* x) - 昨天x))/ n

     
      
  • x =您的时间序列中的值
  •   
  • n =您目前分析的值的数量。
  •   

但是,要将Power Sum Average公式转换为窗口种类,您需要将公式调整为以下内容:

  

PSA今天= PSA昨天+(((今天x今天* x) - (x昨天* x昨天)/ n

     
      
  • x =您的时间序列中的值
  •   
  • n =您目前分析的值的数量。
  •   

您还需要滚动简单移动平均线公式:

  

今天SMA =昨天的SMA +((今天是x - 今天是 - n)/ n

     
      
  • x =您的时间序列中的值
  •   
  • n =用于滚动窗口的时间段。
  •   

从那里你可以计算滚动人口差异:

  

今天人口变量=(PSA今天* n - n * SMA今天* SMA今天)/ n

滚动样本差异:

  

今天的样本Var =(PSA今天* n - n * SMA今天* SMA今天)/(n - 1)

几年前,我在博客文章Running Variance中介绍了此主题以及示例Python代码。

希望这有帮助。

  

请注意:我提供了所有博文和数学公式的链接   在Latex(图像)中得到这个答案。但是,由于我的声誉很低(<   10);我只限于2个超链接,绝对没有图像。抱歉   对这个。希望这不会带走内容。

答案 1 :(得分:17)

我一直在处理同样的问题。

平均值很容易迭代计算,但您需要将值的完整历史记录保存在循环缓冲区中。

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;

我已经改编了Welford的算法,它适用于我测试过的所有值。

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;

要获得当前差异,只需将varSum除以窗口大小:variance = varSum / window_size;

答案 2 :(得分:7)

如果您更喜欢代码而不是单词(严重基于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);
    }
}

答案 3 :(得分:5)

这是一种具有O(log k) - 时间更新的分而治之的方法,其中k是样本数。它应该是相对稳定的,因为成对求和和FFT是稳定的,但它有点复杂,常数不是很大。

假设我们的序列A长度为m,平均值为E(A),方差为V(A),序列B的长度为n平均E(B)和方差V(B)。让C成为AB的串联。我们有

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

现在,将元素填充到红黑树中,其中每个节点都使用以该节点为根的子树的均值和方差进行修饰。插入右侧;在左边删除。 (由于我们只是访问两端,一个splay树可能O(1)摊销,但我猜你的应用是一个问题。)如果k是在编译时已知,您可以展开内循环FFTW样式。

答案 4 :(得分:4)

实际上,Welfords算法可以很容易地使AFAICT适应于计算加权方差。 通过将权重设置为-1,您应该能够有效地取消元素。我没有检查数学是否允许负重量,但初看起来它应该!

我确实使用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);
}
}

与精确的双通算法相比,我得到了大约14位的精度;这与双打的预期差不多。请注意,由于额外的划分,Welford 会带来一些计算成本 - 它需要的时间大约是精确的两遍算法的两倍。如果您的窗口大小很小,那么实际重新计算均值可能会更加明智,然后在第二次传递方差时间。

我已将此实验作为单元测试添加到ELKI,您可以在此处查看完整的来源:http://elki.dbs.ifi.lmu.de/browser/elki/trunk/test/de/lmu/ifi/dbs/elki/math/TestSlidingVariance.java 它还与精确的两次通过方差进行比较。

但是,在偏斜的数据集上,行为可能会有所不同。该数据集显然是均匀分布的;但我也尝试了一个排序的数组,它起作用了。

答案 5 :(得分:2)

我知道这个问题已经过时了,但是如果有人对此感兴趣,请遵循python代码。它的灵感来自johndcook博客文章,@ Joachim's,@ DanS的代码和@Jaime评论。下面的代码仍然为小数据窗口大小提供了小的不精确性。享受。

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

答案 6 :(得分:1)

我期待在这方面被证明是错误的,但我不认为这可以“快速”完成。也就是说,计算的很大一部分是跟踪窗户上的EV,这可以很容易地完成。

我将提出问题:您确定需要窗口功能吗?除非您使用非常大的窗口,否则最好只使用众所周知的预定义算法。

答案 7 :(得分:1)

我想跟踪你的20个样本,Sum(X ^ 2从1..20)和Sum(X从1..20)然后在每次迭代中连续重新计算两个总和是不够有效的?可以重新计算新的方差,而不必每次都加上,平方等所有样本。

如:

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

答案 8 :(得分:1)

这是另一个O(log k)解决方案:找到原始序列的正方形,然后求和,然后是四元组等。(你需要一点缓冲才能有效地找到所有这些。)然后将您需要的值加起来以获得答案。例如:

|||||||||||||||||||||||||  // 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.

现在你使用你的标准E(x ^ 2)-E(x)^ 2公式并且你已经完成了。(如果你需要很好的稳定性,那么小的数字就不行了;这是假设只是累积滚动错误导致问题。)

也就是说,在大多数架构上,这几天总共20个平方数字是非常。如果你做得更多 - 比方说,几百 - 一个更有效的方法显然会更好。但我不确定蛮力不是去这里的方式。

答案 9 :(得分:1)

对于只有20个值,调整暴露的方法here是微不足道的(尽管我没说快)。

您可以简单地选取20个这些RunningStat类的数组。

流的前20个元素有点特殊,但是一旦完成,它就会变得更加简单:

  • 当一个新元素到达时,清除当前的RunningStat实例,将该元素添加到所有20个实例,并递增“counter”(模20),用于标识新的“完整”RunningStat实例
  • 在任何特定时刻,您都可以参考当前的“完整”实例来获取正在运行的变体。

你会明显注意到这种方法并不是真正可扩展的......

您还可以注意到,我们保留的数字有所减少(如果您使用RunningStat完整课程)。一个显而易见的改进是直接保持20个MkSk

我无法想到使用这种特殊算法的更好的公式,我担心它的递归公式会牵连我们的手。

答案 10 :(得分:0)

这只是DanS提供的优秀答案的一小部分。以下等式用于从窗口中移除最旧的样本并更新均值和方差。这很有用,例如,如果您想在输入数据流的右边缘附近采用较小的窗口(即只删除最旧的窗口样本而不添加新样本)。

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)

此处,x_old是您要删除的窗口中最旧的样本。