滚动方差算法


69

我正在尝试找到一种有效的,数值稳定的算法来计算滚动方差(例如,一个20周期滚动窗口的方差)。我知道Welford算法可以有效地计算数字流的运行方差(它只需要一次通过),但是不确定是否可以将其应用于滚动窗口。我也想解决方案,以避免在顶部讨论的准确性问题,这篇文章由John D.库克。任何语言的解决方案都可以。


1
+1(提及Welford算法);我知道它在Knuth,但不知道原始来源
Jason S

2
您好,您最终做了什么?您是否采用了Chan的算法?顺便说一句,当使用“朴素”方法(跟踪值的总和及其平方)时,kahan sum难道不能够克服数值不稳定性吗?
亚瑟

Answers:


26

我也遇到了这个问题。在计算运行累积方差时有很多不错的文章,例如John Cooke的Accurately计算运行方差文章和Digital Explorations的文章,用于计算样本和总体方差,协方差和相关系数的Python代码。只是找不到适合滚动窗口的任何内容。

Subluminal Messages的“运行标准偏差”帖子对于使滚动窗口公式正常工作至关重要。吉姆采用了值平方差的幂和,而韦尔福德采用的是均值平方差之和。公式如下:

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

  • x =时间序列中的值
  • n =到目前为止已分析的值数。

但是,要将“求和平均值”公式转换为加窗变量,您需要将该公式调整为以下内容:

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

  • x =时间序列中的值
  • n =到目前为止已分析的值数。

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

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

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

从那里您可以计算滚动人口方差:

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

或滚动样本差异:

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

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

希望这可以帮助。

请注意:针对此答案,我提供了指向Latex(图像)中所有博客文章和数学公式的链接。但是,由于我的声誉低(<10);我仅限于2个超链接,而且绝对没有图像。为此表示歉意。希望这不会脱离内容。


1
在此公式中:Population Var today = (PSA today * n - n * SMA today * SMA today) / n-为什么不删除nPopulation Var today = (PSA today - SMA today * SMA today)
astef

2
由于对公式中的样本进行平方,因此该算法显示出OP试图避免的非常数值上的误差。
marton78

2
是的,这不是数值稳定的方法。正确答案最接近的是下面的@DanS。
Jaime


22

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

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

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;


5
这样做可能会稍微稳定一些varSum += (x_new + x_old - mean - new_mean) * (x_new - x_old),在此情况下x_old = xs[next_index],您mean * new_mean从减去要更新的两个项目中删除了一个可能较大的被加数varSum。除此之外,这是这里最正确的答案,可惜没有得到更多的爱。
海梅

2
为了阐明Jaime的答案,他做了一些代数,采用了DanSvarSum方程并分配了乘法。有些条款取消了,但您还必须执行加法 x_new * x_old - x_new * x_old运算才能得出他的结果
Ryan J McCall '18

1
很晚的评论:你为什么window_size不去潜水window_size-1。换句话说:为什么不使用贝塞尔校正。我注意到约翰·库克(John D. Cook)确实在他的运行方差代码中包括了贝塞尔的更正。
hansfn

您不能varSum仅仅将其完全删除variance += (x_new + x_old - mean - new_mean) * (x_new - x_old) / window_size吗?
Guiorgy

8

如果您喜欢代码而不是单词(很大程度上基于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);
    }
}

6

实际上,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 ,它还与精确的两遍方差进行比较。

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

更新:我们发表了一篇论文,详细介绍了(协方差)的不同加权方案:

Schubert,Erich和Michael Gertz。“ (协)方差的数值稳定并行计算。 ”第30届科学与统计数据库管理国际会议论文集。ACM,2018年。(获得SSDBM最佳论文奖。)

这也讨论了如何使用加权来并行化计算,例如使用AVX,GPU或在群集上。


将ELKI MeanVarance.java类移植到JS,添加了一个值缓冲区,并使用-1的权重来删除值。我发现结果精度根据您通过累加器运行多少个值而有所不同。通过它运行1-10M值后,我看到的精度约为12位。(即“足够好”)感谢您使用-1砝码的提示!
broofa

如果您需要更高的精度,则可能需要使用Kahan求和或Shewchuk算法。这些使用额外的浮点数来存储丢失的数字,因此可以提供更高的精度。但是实现变得更加混乱和缓慢。有关更多详细信息,请参阅我添加到帖子中的参考。
埃里希·舒伯特

5

这是具有O(log k)-time更新的分而治之的方法,这里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)))

现在,将元素填充到一棵红黑树中,其中每个节点都装饰有以该节点为根的子树的均值和方差。在右边插入;在左侧删除。(由于我们仅访问末端,因此可能O(1) 摊销八卦树,但我猜想摊销对您的应用程序来说是个问题。)如果k在编译时已知,则可以展开内循环FFTW样式。


(注意:除非k非常大,否则计算q = 1-p很好。)
userOVER9000 2011年

1
好的,这基本上是Chan等人在Wikipedia上描述的并行算法。这就是我不向下滚动所得到的...
userOVER9000

您能否更详细地说明如何将该算法应用于移动窗口的方差?我对Chan等人的方法有点熟悉,但是认为它是一种用于计算整个样本的单个方差的单次通过方法,其附加优点是可以将问题分解为多个并行运行的部分。
Abiel

鉴于零件的统计信息,Chan等人给出了一种计算零件串联的统计信息的方法。高层的想法是维护零件的集合(实际上只是零件的统计信息),以使任何窗口都是O(log k)零件的串联。一种方法是使用平衡的二叉树,但是正如Rex指出的那样,这是过大的,我们只需要维护尺寸为2的幂(例如[0,1),[1,2),[0 ,2),[2、3),[3、4),[2、4),[0、4)等)
userOVER9000 2011年

3

我知道这个问题很旧,但是如果有人对这里感兴趣,请遵循python代码。它的灵感来自johndcook博客文章,@ Joachim,@ 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))

1
感谢python中的想法综合。我不喜欢WIN_SIZE - 1在输入else块的情况下窗口的大小如何。因此,如果调用WIN_SIZEpush为10,然后追加,则由于使用了双端队列构造器选项,它仍为10,然后在elsepopleft中将大小进一步减小到9 maxlen=WIN_SIZE + 1。还是不使用该maxlen选项。另外,可以删除n变量并使用len(self.windows)
瑞安·麦考

1
get_var方法中的分母应该是self.nlen(self.windows)
瑞恩Ĵ麦考

1

我期待在此方面被证明是错误的,但是我认为这不能“迅速”完成。也就是说,计算的很大一部分是在窗口上跟踪EV,这很容易做到。

我将离开一个问题:您确定需要一个窗口函数吗?除非使用非常大的窗口,否则最好使用众所周知的预定义算法。


1

我猜想跟踪一下您的20个样本,即Sum(X.2 from 1..20)和Sum(X from 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

1
我相信此解决方案容易受到我原始帖子(johndcook.com/standard_deviation.html)中的链接中提到的稳定性问题的影响。特别是,当输入值和大且它们的差小于结果时,实际上可能为负。我将无法控制输入,因此我宁愿避免这种方法。
Abiel

哦,我懂了。关于输入您有什么可以说的吗?有可能的使用?您只可以抛出更多的位(64位浮点数,任意精度算术等)是否有问题?如果您将输入数字压倒大数,则舍入误差会消失,不是吗?
约翰

同意-这有稳定性问题。想象一下1,000,000.0附近的1000个样本,然后是零附近的20个样本。
詹森·S

@Jason S:滚动方差就是它。从一百万到零的转变可能会发生很多事情,但这就是野兽的本质。那样,无论如何,当发生更改时,1000〜1百万个值中的前980个就不在画面中。我的评论建议,如果您的计算中有足够的重要数字,那么这都不重要。
约翰,

输入实际上可以是任何东西。值的大小肯定可以达到数万亿,虽然原始数据的准确度只有几个小数点,但用户将能够在计算方差之前转换其数据(例如,除以任何标量)。
Abiel

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个平方数的速度非常快。如果您做得更多(例如几百种),那么更有效的方法显然会更好。但是我不确定强暴不是走这条路的方法。


4
“使用您的标准E(x ^ 2)-E(x)^ 2公式”不,不要;它甚至不是远程稳定的。改编更好的算法之一。
userOVER9000

@ userOVER9000-您为什么担心20项以上的稳定性?积累了数百万个条目的累积错误是一个问题(尤其是在创建滚动窗口时),但这不是问题所在。
Rex Kerr

我很担心,因为这是一个问题。继续阅读Wikipedia文章,如果仍然不满意,请尝试计算N(1,1e-10)的20个iid样本的方差。
userOVER9000 2011年

对于具有合理单位和原点的任何现实数据集,我还没有看到这实际上是一个问题,但是很公平,如果那是OP想要的...
Rex Kerr

1

对于只有20个值,适应这里公开的方法很简单(不过我没有说很快)。

您可以简单地选择20个此类的数组RunningStat

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

  • 当一个新元素到达时,清除当前RunningStat实例,将该元素添加到所有20个实例中,并递增标识新“完整”RunningStat实例的“计数器”(模20)
  • 在任何给定的时刻,您都可以查询当前的“完整”实例以获取运行的变体。

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

您还可以注意到,我们保留的数字有些冗余(如果您参加RunningStat全班学习的话)。一个明显的改善将是保持20持续MkSk直接。

我无法想到使用此特定算法的更好公式,恐怕其递归公式在某种程度上会束缚我们的双手。


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是您要删除的窗口中最旧的示例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.