私は、ローリング分散(たとえば、20期間のローリングウィンドウにわたる分散)を計算するための効率的で数値的に安定したアルゴリズムを見つけようとしています。 Welfordアルゴリズム は数字のストリームの実行分散を効率的に計算します(1パスのみが必要です)が、これがローリングウィンドウに適応できるかどうかはわかりません。また、ジョンD.クックによる この記事 の冒頭で説明した精度の問題を回避する解決策が欲しいです。どの言語のソリューションでも大丈夫です。
私もこの問題に出くわしました。ジョンクックの実行中の分散を正確に計算するポストおよびDigital explorationsからのポストPythonコードなど、実行中の累積分散の計算には素晴らしい投稿がありますサンプルおよび母集団の分散、共分散、相関係数の計算用。ローリングウィンドウに適合したものは見つかりませんでした。
Running Standard Deviations Subluminal Messagesによる投稿は、ローリングウィンドウ式を機能させるために重要でした。ジムは、値の二乗差の累乗和と、平均の二乗差の和を使用するウェルフォードのアプローチを取ります。次のような式:
今日の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を計算できます:
今日の母集団=(今日のPSA * n-n *今日のSMA *今日のSMA)/ n
または、ローリングサンプルの分散:
今日のサンプル変数=(今日のPSA * n-n *今日のSMA *今日のSMA)/(n-1)
このトピックは、数年前のブログ投稿のサンプルPythonコード、---(Running Variance )とともに説明しました。
お役に立てれば。
注:この回答では、すべてのブログ投稿とラテックスの数式(画像)へのリンクを提供しました。しかし、私の評判が低いため(<10)。ハイパーリンクは2つに制限されており、画像はまったくありません。これにつきましては申し訳ございません。これがコンテンツから離れないことを願っています。
私は同じ問題に取り組んできました。
Meanは反復計算が簡単ですが、値の完全な履歴を循環バッファーに保持する必要があります。
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;
言葉よりもコードを好む場合(主に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);
}
}
以下は、O(log k)
時間の更新がある分割統治アプローチです。ここで、k
はサンプル数です。ペアワイズ加算とFFTが安定しているのと同じ理由で、比較的安定しているはずですが、少し複雑で、定数は大きくありません。
平均E(A)
および分散V(A)
の長さA
のシーケンスm
と、平均E(B)
および分散V(B)
の長さB
のシーケンスn
があるとします。 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)))
次に、要素を赤黒ツリーに詰め込みます。各ノードには、そのノードをルートとするサブツリーの平均と分散が装飾されています。右側に挿入します。左側で削除します。 (最後にアクセスするだけなので、スプレイツリーmight be O(1)
が償却されますが、アプリケーションでは償却が問題であると推測します。)k
がコンパイル時にわかっている場合-time、おそらく内側のループFFTWスタイルを展開できます。
実際、Welfordsアルゴリズムは、weightedVarianceの計算に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);
}
}
正確な2パスアルゴリズムと比較して、約14桁の精度が得られます。これは、ダブルスから予想されるものとほぼ同じです。 Welforddoesは、余分な除算のためにある程度の計算コストがかかることに注意してください。正確な2パスアルゴリズムの約2倍の時間がかかります。ウィンドウサイズが小さい場合、平均を実際に再計算し、2回目のパスで分散every時間を再計算する方がはるかに賢明です。
この実験をELKIの単体テストとして追加しました。完全なソースはここにあります: http://elki.dbs.ifi.lmu.de/browser/elki/trunk/test/de/lmu/ifi /dbs/elki/math/TestSlidingVariance.Java また、正確な2パス分散と比較します。
ただし、歪んだデータセットでは、動作が異なる場合があります。このデータセットは明らかに均一に分布しています。しかし、ソートされた配列も試しましたが、うまくいきました。
更新:(共)分散の異なる重み付けスキームに関する詳細を含む論文を公開しました:
シューベルト、エーリッヒ、マイケルゲルツ。 「(co-)分散の数値的に安定した並列計算。」科学および統計データベース管理に関する第30回国際会議の議事録。 ACM、2018。(SSDBMベストペーパー賞を受賞。)
また、AVX、GPU、またはクラスターなどで、計算を並列化するために重み付けを使用する方法についても説明します。
私はこの質問が古いことを知っていますが、他の誰かがここに興味がある場合は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))
20の値の場合、公開されているメソッド here (ただし、高速とは言いませんでした)を適応させるのは簡単です。
これらのRunningStat
クラスの20個の配列を簡単に選択できます。
ストリームの最初の20要素はやや特別ですが、これが完了すると、はるかに簡単になります。
RunningStat
インスタンスをクリアし、20個すべてのインスタンスに要素を追加し、新しい「フル」RunningStat
インスタンスを識別する「カウンター」(モジュロ20)をインクリメントしますこのアプローチは実際にはスケーラブルではないことに明らかに気付くでしょう...
また、保持する数値には多少の冗長性があることに注意することもできます(RunningStat
フルクラスを使用する場合)。明らかな改善は、20個のMk
とSk
を直接保持することです。
この特定のアルゴリズムを使用したより良い式を考えることはできません。その再帰的な定式化が私たちの手を多少結びつけるのではないかと心配しています。
これについて間違っていることが証明されるのを楽しみにしていますが、これを「迅速に」できるとは思いません。とはいえ、計算の大部分は、簡単に実行できるウィンドウ上でEVを追跡することです。
質問を残します:need windowed function?非常に大きなウィンドウで作業している場合を除き、よく知られている定義済みのアルゴリズムを使用することをお勧めします。
別のO(log k)
解決策は次のとおりです。findは元のシーケンスを2乗し、次にペアを合計し、次に4倍などになります。答えを得るために必要な値を増やしてください。例えば:
||||||||||||||||||||||||| // 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の2乗数の合計は、ほとんどのアーキテクチャでvery高速です。より多くの作業を行っている場合(たとえば、数百)、より効率的な方法のほうが明らかに優れています。しかし、ブルートフォースがここに行く方法ではないことはわかりません。
20個のサンプル、Sum(X ^ 2 from 1..20)、およびSum(X from 1..20)を追跡し、各反復で2つの合計を連続的に再計算することは十分に効率的ではないと思います。毎回すべてのサンプルを加算、二乗などすることなく、新しい分散を再計算することができます。
次のように:
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
これは、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は、削除するウィンドウ内の最も古いサンプルです。