我正在尝试寻找一种高效、数值稳定的算法来计算滚动方差(例如,一个20周期的滚动窗口内的方差)。我知道Welford算法可以有效地计算数字流的运行方差(只需要一次遍历),但不确定是否可以将其适应于滚动窗口。我还希望解决方案避免John D. Cook在这篇文章顶部讨论的精度问题。任何语言的解决方案都可以。
我正在尝试寻找一种高效、数值稳定的算法来计算滚动方差(例如,一个20周期的滚动窗口内的方差)。我知道Welford算法可以有效地计算数字流的运行方差(只需要一次遍历),但不确定是否可以将其适应于滚动窗口。我还希望解决方案避免John D. Cook在这篇文章顶部讨论的精度问题。任何语言的解决方案都可以。
我一直在处理相同的问题。
计算平均数可以采用迭代的方法,但需要在循环缓冲区中保留完整的数值历史记录。
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;
varSum
时,使用varSum += (x_new + x_old - mean - new_mean) * (x_new - x_old)
可能会更加稳定,其中x_old = xs[next_index]
,因为您从两个相减的项目中去除了一个有可能很大的mean * new_mean
项。 除此之外,在这里这是最正确的答案,非常遗憾它没有得到更多的认可。 - Jaimex_new * x_old - x_new * x_old
的技巧来得出他的结果。 - Ryan J McCallwindow_size
而不是 window_size-1
。换句话说:为什么你不使用贝塞尔校正。我注意到 John D. Cook 在他的运行方差代码中包括了贝塞尔校正。 - hansfnvariance += (x_new + x_old - mean - new_mean) * (x_new - x_old) / window_size
来完全移除 varSum
? - Guiorgy我也遇到了这个问题。 有一些关于计算累积方差的很好的帖子,例如John Cooke的 准确计算运行方差 帖子和Digital explorations的帖子,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今天-x今天-n) / n
- x =时间序列中的值
- n =用于您的滚动窗口的期间。
从那里,您可以计算Rolling Population Variance:
Population Var今天=(PSA今天*n-n*SMA今天*SMA今天)/ n
或者使用滚动样本方差:
今天的样本方差=(今天的PSA * n - n * 今天的SMA * 今天的SMA)/(n-1)
我在几年前的博客文章中涵盖了这个主题,并提供了示例Python代码,Running Variance。
希望这可以帮到你。
请注意:我为这个答案提供了所有博客文章和LaTeX数学公式的链接(图片)。但由于我的声望太低(<10),我只能提供2个超链接和绝对没有图片。对此我深感抱歉,希望这不会影响内容。
Population Var today = (PSA today * n - n * SMA today * SMA today) / n
,为什么不去掉 n
?Population Var today = (PSA today - SMA today * SMA today)
。 - astef如果你更喜欢代码而不是文字(基于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);
}
}
实际上,据我所知,Welford算法可以轻松地适应于计算加权方差。通过将权重设置为-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);
}
}
我知道这个问题很久了,但如果还有其他人感兴趣,这里是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))
WIN_SIZE - 1
。因此,如果在调用push
时WIN_SIZE
为10并且我们进行了附加,则由于使用了deque构造函数选项,它仍然为10,然后在else
块中popleft
会进一步减小大小到9。所以也许可以使用maxlen=WIN_SIZE + 1
?或者不使用maxlen
选项。此外,可以放弃n
变量并使用len(self.windows)
。 - Ryan J McCallget_var
方法中,分母应该是 self.n
或者 len(self.windows)
。 - Ryan J McCallO(log k)
的更新,其中 k
是样本数。由于与成对求和和快速傅里叶变换(FFT)相同的原因,它应该是相对稳定的,但它有点复杂,常数并不好。m
、均值为 E(A)
、方差为 V(A)
的序列 A
,以及一个长度为 n
、均值为 E(B)
、方差为 V(B)
的序列 B
。让 C
成为 A
和 B
的串联。我们有: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一样展开内部循环。
这只是对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是您希望移除的窗口中最旧的样本。
具体操作:
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
||||||||||||||||||||||||| // 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个平方数求和是非常快速的。如果您要进行更多操作——比如说,几百次——更高效的方法显然更好。但我不确定这里是否采用蛮力法才是正确的方式。
我希望我是错的,但我认为这不能“快速”完成。话虽如此,计算的大部分是跟踪窗口内的期望值,这可以轻松完成。
我留下一个问题:你确定你需要一个窗口函数吗?除非你正在使用非常大的窗口,否则最好只使用一个众所周知的预定义算法。