平均絶対偏差と大規模なデータセットのオンラインアルゴリズム


16

私にはちょっとした問題があります。多変量時系列のオンライン取得プロセスの手順を作成する必要があります。すべての時間間隔(たとえば1秒)で、基本的にサイズNの浮動小数点ベクトルである新しいサンプルを取得します。実行する必要がある操作は少し複雑です。

  1. 新しいサンプルごとに、要素の合計が1になるようにベクトルを正規化して、そのサンプルのパーセントを計算します。

  2. 同じ方法で平均パーセントベクトルを計算しますが、過去の値を使用します。

  3. 過去の各値について、ステップ2で計算されたグローバル平均パーセントベクトルを使用して、そのサンプルに関連するパーセントベクトルの絶対偏差を計算します。この方法では、絶対偏差は常に0(ベクトルが平均に等しい場合)ベクトル)および2(完全に異なる場合)。

  4. 前のすべてのサンプルの偏差の平均を使用して、平均絶対偏差を計算します。これは、0〜2の数値です。

  5. 平均絶対偏差を使用して、新しいサンプルが他のサンプルと互換性があるかどうかを検出します(その絶対偏差を、ステップ4で計算されたセット全体の平均絶対偏差と比較します)。

新しいサンプルが収集されるたびにグローバル平均が変化するため(平均絶対偏差も変化するため)、データセット全体を複数回スキャンせずにこの値を計算する方法はありますか?(グローバル平均パーセントの計算に1回、絶対偏差の収集に1回)。わかりました。各次元の合計を保存するために一時的なベクトルを使用するだけなので、セット全体をスキャンせずにグローバル平均を計算するのは非常に簡単ですが、平均絶対偏差はどうですか?その計算にはabs()演算子が含まれているため、過去のすべてのデータにアクセスする必要があります!

ご協力いただきありがとうございます。

回答:


6

不正確さを受け入れることができる場合、この問題はカウントをビニングすることで簡単に解決できます。つまり、大きな数(たとえばM = 1000)を選択し、整数ビンB i ji = 1 Mおよびj = 1 Nに初期化します。ここでNはベクトルサイズで、ゼロとして設定します。次に、パーセントベクトルのk番目の観測値が表示されたら、このベクトルの番目の要素が間にある場合、B i jをインクリメントします。MM=1000Bi,ji=1Mj=1NNkBi,jj(i1)/Mおよび、ベクトルの要素をループします。(入力ベクトルは負でないと仮定しているため、「パーセント」を計算するとき、ベクトルは範囲にあります。)/MN[01]

どの時点でも、ビンから平均ベクトルと平均絶対偏差を推定できます。観察後ようなベクターを、平均番目の要素は、によって推定される ˉ X J = 1Kjおよび平均絶対偏差のj番目の要素は1

バツ¯j=1K1/2MBj
j
1K|バツj¯1/2M|Bj

編集:これは、経験的な密度推定を構築する、より一般的なアプローチの特定のケースです。これは、多項式、スプラインなどを使用して実行できますが、ビニングアプローチが最も簡単に記述および実装できます。


うわー、本当に面白いアプローチ。私はそれについて知らなかったので、心に留めておきます。残念ながら、この場合、メモリ使用量の観点から非常に制限された要件があるため、動作しません。したがって、Mは本当に小さくなければならず、精度の損失が多すぎると思います。
ジャンルカ

@gianluca:大量のデータ、2。限られたメモリリソース、3。高精度の要件があるようです。この問題がなぜあなたを驚かせているのか、私にはわかります!おそらく、@ kwakで述べたように、スプレッドの他の測定値を計算できます:MAD、IQR、標準偏差。これらにはすべて、問題に役立つアプローチがあります。
みすぼらしいシェフ

gianluca:>必要なメモリ、配列、精度について、より定量的な考えを教えてください。しかし、あなたの質問が@stackoverflowで最もよく答えられるかもしれません。
user603

4

私は(ノート、このプログラマが接近し、ない統計学者、そうindubitablyのような巧妙なトリックがあるかもしれない、適度に効率的に計算赦免偏差に、過去に以下のアプローチを使用しましたshabbychefの方が効率的かもしれません)。

警告:これはオンラインアルゴリズムではありません。O(n)メモリが必要です。さらに、次のO(n)ようなデータセットに対して[1, -2, 4, -8, 16, -32, ...](つまり、完全な再計算と同じ)の最悪の場合のパフォーマンスがあります。[1]

ただし、多くのユースケースで引き続き良好に機能するため、ここに投稿する価値があります。たとえば、各アイテムが到着するときに、-100から100までの10000個の乱数の絶対偏差を計算するために、アルゴリズムは1秒未満で完了しますが、完全な再計算には17秒以上かかります(私のマシンでは、マシンごとに異なり、入力データに応じて)。ただし、ベクトル全体をメモリに保持する必要がありますが、これは用途によっては制約になる場合があります。アルゴリズムの概要は次のとおりです。

  1. 過去の測定値を保存するための単一のベクトルを持つ代わりに、3つの並べ替えられた優先度キュー(最小/最大ヒープのようなもの)を使用します。これらの3つのリストは、入力を3つに分割します。平均より大きいアイテム、平均より小さいアイテム、平均に等しいアイテムです。
  2. (ほぼ)アイテムを追加するたびに平均値が変化するため、パーティションを再分割する必要があります。重要なことは、パーティションのソートされた性質です。つまり、リスト内のすべてのアイテムをスキャンしてパーティションを分割するのではなく、移動しているアイテムを読み取るだけで済みます。最悪の場合、これにはO(n)移動操作が必要になりますが、多くのユースケースではそうではありません。
  3. 巧妙な簿記を使用することで、パーティションの再分割時や新しいアイテムの追加時に常に逸脱が正しく計算されるようにできます。

Pythonのサンプルコードを以下に示します。リストに追加できるのはアイテムのみであり、削除はできないことに注意してください。これは簡単に追加できますが、私がこれを書いたとき、私はそれを必要としませんでした。プライオリティキューを自分で実装するのではなく、私が使用しているSortedListのをダニエルStutzbachの優れたからblistパッケージの使用、B +ツリーが内部的です。

MITライセンスの下でライセンスされているこのコードを検討してください。大幅に最適化または洗練されたわけではありませんが、過去に私のために機能しました。新しいバージョンはこちらから入手できます。質問がある場合、またはバグを見つける場合はお知らせください。

from blist import sortedlist
import operator

class deviance_list:
    def __init__(self):
        self.mean =  0.0
        self._old_mean = 0.0
        self._sum =  0L
        self._n =  0  #n items
        # items greater than the mean
        self._toplist =  sortedlist()
        # items less than the mean
        self._bottomlist = sortedlist(key = operator.neg)
        # Since all items in the "eq list" have the same value (self.mean) we don't need
        # to maintain an eq list, only a count
        self._eqlistlen = 0

        self._top_deviance =  0
        self._bottom_deviance =  0

    @property
    def absolute_deviance(self):
        return self._top_deviance + self._bottom_deviance

    def append(self,  n):
        # Update summary stats
        self._sum += n
        self._n +=  1
        self._old_mean =  self.mean
        self.mean =  self._sum /  float(self._n)

        # Move existing things around
        going_up = self.mean > self._old_mean
        self._rebalance(going_up)

        # Add new item to appropriate list
        if n >  self.mean:
            self._toplist.add(n)
            self._top_deviance +=  n -  self.mean
        elif n == self.mean: 
            self._eqlistlen += 1
        else:
            self._bottomlist.add(n)
            self._bottom_deviance += self.mean -  n


    def _move_eqs(self,  going_up):
        if going_up:
            self._bottomlist.update([self._old_mean] *  self._eqlistlen)
            self._bottom_deviance += (self.mean - self._old_mean) * self._eqlistlen
            self._eqlistlen = 0
        else:
            self._toplist.update([self._old_mean] *  self._eqlistlen)
            self._top_deviance += (self._old_mean - self.mean) * self._eqlistlen
            self._eqlistlen = 0


    def _rebalance(self, going_up):
        move_count,  eq_move_count = 0, 0
        if going_up:
            # increase the bottom deviance of the items already in the bottomlist
            if self.mean !=  self._old_mean:
                self._bottom_deviance += len(self._bottomlist) *  (self.mean -  self._old_mean)
                self._move_eqs(going_up)


            # transfer items from top to bottom (or eq) list, and change the deviances
            for n in iter(self._toplist):
                if n < self.mean:
                    self._top_deviance -= n -  self._old_mean
                    self._bottom_deviance += (self.mean -  n)
                    # we increment movecount and move them after the list
                    # has finished iterating so we don't modify the list during iteration
                    move_count +=  1
                elif n == self.mean:
                    self._top_deviance -= n -  self._old_mean
                    self._eqlistlen += 1
                    eq_move_count +=  1
                else:
                    break
            for _ in xrange(0,  move_count):
                self._bottomlist.add(self._toplist.pop(0))
            for _ in xrange(0,  eq_move_count):
                self._toplist.pop(0)

            # decrease the top deviance of the items remain in the toplist
            self._top_deviance -= len(self._toplist) *  (self.mean -  self._old_mean)
        else:
            if self.mean !=  self._old_mean:
                self._top_deviance += len(self._toplist) *  (self._old_mean -  self.mean)
                self._move_eqs(going_up)
            for n in iter(self._bottomlist): 
                if n > self.mean:
                    self._bottom_deviance -= self._old_mean -  n
                    self._top_deviance += n -  self.mean
                    move_count += 1
                elif n == self.mean:
                    self._bottom_deviance -= self._old_mean -  n
                    self._eqlistlen += 1
                    eq_move_count +=  1
                else:
                    break
            for _ in xrange(0,  move_count):
                    self._toplist.add(self._bottomlist.pop(0))
            for _ in xrange(0,  eq_move_count):
                self._bottomlist.pop(0)

            # decrease the bottom deviance of the items remain in the bottomlist
            self._bottom_deviance -= len(self._bottomlist) *  (self._old_mean -  self.mean)


if __name__ ==  "__main__":
    import random
    dv =  deviance_list()
    # Test against some random data,  and calculate result manually (nb. slowly) to ensure correctness
    rands = [random.randint(-100,  100) for _ in range(0,  1000)]
    ns = []
    for n in rands: 
        dv.append(n)
        ns.append(n)
        print("added:%4d,  mean:%3.2f,  oldmean:%3.2f,  mean ad:%3.2f" %
              (n, dv.mean,  dv._old_mean,  dv.absolute_deviance / dv.mean))
        assert sum(ns) == dv._sum,  "Sums not equal!"
        assert len(ns) == dv._n,  "Counts not equal!"
        m = sum(ns) / float(len(ns))
        assert m == dv.mean,  "Means not equal!"
        real_abs_dev = sum([abs(m - x) for x in ns])
        # Due to floating point imprecision, we check if the difference between the
        # two ways of calculating the asb. dev. is small rather than checking equality
        assert abs(real_abs_dev - dv.absolute_deviance) < 0.01, (
            "Absolute deviances not equal. Real:%.2f,  calc:%.2f" %  (real_abs_dev,  dv.absolute_deviance))

[1]症状が続く場合は、医師にご相談ください。


2
私は何かが欠けています:「メモリ内のベクトル全体を維持する」必要がある場合、これは「オンライン」アルゴリズムとしてどのように修飾されますか?
whuber

@whuberいいえ、何かを見逃していない、私はそれがオンラインアルゴリズムではないと思います。O(n)メモリが必要であり、最悪の場合、追加される各アイテムにO(n)時間かかります。正規分布データ(およびおそらく他の分布)では、非常に効率的に機能します。
fmark

3

バツバツバツss2/π


それは面白いアイデアです。おそらく、外れ値のオンライン検出でそれを補完し、それらを使用して、進行中に推定値を変更できます。
whuber

おそらく、2番目の回答で文書化した標準偏差をオンラインで計算するために、ウェルフォードの方法を使用できます。
fmark

1
ただし、この方法では、明示的なMADなどの推定器の堅牢性が失われる可能性があることに注意する必要があります。
クォーツ

2

MAD(x)は、2つの同時中央値計算であり、それぞれがビンメディアンアルゴリズムを介してオンラインで作成できます。

CおよびFORTRANコードと同様に、関連する論文をこちらでオンライン見つけることができます。

(これは、メモリを節約するために、Shabbychefの巧妙なトリックの上に巧妙なトリックを使用するだけです)。

補遺:

変位値を計算するための古いマルチパス方式が多数存在します。一般的なアプローチは、ストリームからランダムに選択された決定論的にサイズ決定された観測値のリザーバーを維持/更新し、このリザーバーで再帰的に変位値を計算することです(このレビューを参照)。この(および関連する)アプローチは、上記で提案したアプローチに取って代わられます。


MADと2つの中央値の関係について詳しく説明してください。
クォーツ

それは本当にMADの公式です: med=1n|バツmed=1n|(したがって2つの中央値)
-user603

ふーむ、実際、この関係が2つの中央値を同時に可能にすることをどのように説明できるかを説明しました。外側の中央値への入力は、内側の計算に追加された各サンプルですべて変化する可能性があるため、これらは私に依存しているようです。それらをどのように並行して実行しますか?
クォーツ

詳細についてはビンメディアンの論文に戻る必要がありますが、中央値の計算値(med=1nバツ)および新しい値 バツn+1 アルゴリズムは計算できます med=1n+1バツ よりもはるかに速い On ビンを識別することにより バツn+1属します。私は、この洞察が狂った計算の外側中央値にどのように一般化されなかったのかわかりません。
user603

1

以下は不正確な近似を提供しますが、不正確さは入力データの分布に依存します。これはオンラインアルゴリズムですが、絶対的な逸脱に近似するだけです。1960年代にウェルフォードによって説明された、オンラインで分散を計算するためのよく知られたアルゴリズムに基づいています。Rに変換された彼のアルゴリズムは次のようになります。

M2 <- 0
mean <- 0
n <- 0

var.online <- function(x){
    n <<- n + 1
    diff <- x - mean
    mean <<- mean + diff / n
    M2 <<- M2 + diff * (x - mean)
    variance <- M2 / (n - 1)
    return(variance)
}

Rの組み込み分散関数と非常によく似た動作をします。

set.seed(2099)
n.testitems <- 1000
n.tests <- 100
differences <- rep(NA, n.tests)
for (i in 1:n.tests){
        # Reset counters
        M2 <- 0
        mean <- 0
        n <- 0

        xs <- rnorm(n.testitems)
        for (j in 1:n.testitems){
                v <- var.online(xs[j])
        }

        differences[i] <- abs(v - var(xs))

}
summary(differences)
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
0.000e+00 2.220e-16 4.996e-16 6.595e-16 9.992e-16 1.887e-15 

アルゴリズムを変更して絶対偏差を計算するには、追加のsqrt呼び出しが必要になります。ただし、sqrt結果に反映される不正確さが導入されます。

absolute.deviance.online <- function(x){
    n <<- n + 1
    diff <- x - mean
    mean <<- mean + diff / n
    a.dev <<- a.dev + sqrt(diff * (x - mean))
    return(a.dev)
}

上記のように計算されたエラーは、分散の計算よりもはるかに大きくなります。

    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
0.005126 0.364600 0.808000 0.958800 1.360000 3.312000 

ただし、ユースケースによっては、この大きさのエラーが許容される場合があります。

historgram of differences


次の理由により、これは正確な答えを与えません。 バツバツ。あなたは前者を計算していますが、OPは後者を望んでいます。
みすぼらしいシェフ

方法が不正確であることに同意します。しかし、私はあなたの不正確さの診断には同意しません。分散を計算するWelfordの方法には、sqrtも含まれていませんが、同様のエラーがあります。しかし、n大きくerror/nなると、驚くほど速く、非常に小さくなります。
fmark

ウェルフォードの方法は、標準偏差ではなく分散を計算しているため、sqrtがありません。sqrtを取ることにより、平均絶対偏差ではなく標準偏差を推定しているように見えます。私は何かが欠けていますか?
みすぼらしいシェフ

@shabbychef Welfordsの各反復は、絶対偏差に対する新しいデータポイントの寄与を計算します。したがって、絶対偏差に戻るために、各寄与の平方根を取ります。たとえば、標準偏差の場合のように後でではなく、偏差合計に追加する前に、デルタの平方根を取ることに注意してください。
fmark

3
問題が発生しました。Welfordsは、この方法の問題を曖昧にします。平均の最終推定値の代わりに、平均のオンライン推定値が使用されています。Welfordの方法は分散が正確(丸めまで)ですが、この方法はそうではありません。問題は不正確さによるものではありませんsqrt。移動平均推定値を使用しているためです。これがいつ壊れるのかを確認するにxs <- sort(rnorm(n.testitems)) は、コードでこれを試してみると(を返すように修正した後a.dev / n)、9%-16%程度の相対エラーが発生します。この方法は大混乱を引き起こす可能性が順列不変、...されないように
shabbychef
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.