均值绝对偏差和大数据集的在线算法


16

我有一个小问题使我感到恐惧。我必须为多元时间序列的在线获取过程编写程序。在每个时间间隔(例如1秒),我都会得到一个新样本,该样本基本上是大小为N的浮点向量。我需要做的操作有些棘手:

  1. 对于每个新样本,我计算该样本的百分位数(通过对向量进行归一化,以使元素总和为1)。

  2. 我以相同的方式计算平均百分比矢量,但使用过去的值。

  3. 对于每个过去的值,我使用在步骤2中计算的全局平均百分比矢量来计算与该样本相关的百分比矢量的绝对偏差。这样,绝对偏差始终为0(当矢量等于平均值​​)之间的数字。向量)和2(当完全不同时)。

  4. 使用所有先前样本的偏差平均值,我计算出平均绝对偏差,该平均值也是0到2之间的一个数字。

  5. 我使用平均绝对偏差来检测新样本是否与其他样本兼容(通过将其绝对偏差与在步骤4计算的整个集合的平均绝对偏差进行比较)。

由于每次收集一个新样本时,全局平均值都会发生变化(因此平均绝对偏差也会发生变化),有没有一种方法可以计算此值而无需多次扫描整个数据集?(一次用于计算总体平均百分比,一次用于收集绝对偏差)。好的,我知道在不扫描整个集合的情况下计算全局平均值绝对容易,因为我只需要使用一个临时矢量来存储每个维的和,那么平均绝对偏差呢?它的计算包括abs()运算符,因此我需要访问所有过去的数据!

谢谢你的帮助。

Answers:


6

如果您可以接受一些不准确性,则可以通过对计数进行装箱来轻松解决此问题。也就是说,选择一些较大的数字(例如M = 1000),然后将一些整数bin B i j初始化为i = 1 Mj = 1 N,其中N是矢量大小,为零。然后当看到ķ一个percentual的矢量的第观察,增量Ĵ如果Ĵ该矢量的第i个元素是之间中号中号=1000一世Ĵ一世=1个中号Ĵ=1个ññķ一世ĴĴ i / M遍历向量的 N个元素。(我假设你的输入矢量都是非负的,所以,当你计算你“percentuals”,所述载体是在范围 [ 0 1 ]。)一世-1个/中号一世/中号ñ[01个]

在任何时间点,您都可以从分箱中估计出平均向量以及平均绝对偏差。观察后这样的载体,所述Ĵ个平均值的元件由估计 ˉ X Ĵ = 1ķĴĴ平均绝对偏差的第i个元素是通过估计1

X¯Ĵ=1个ķ一世一世-1个/2中号一世Ĵ
Ĵ
1Ki|Xj¯i1/2M|Bi,Ĵ

编辑:这是一种更一般的方法的特定情况,在该方法中,您将建立经验密度估计。可以使用多项式,样条线等来完成,但是合并方法最容易描述和实现。


哇,真的很有趣。我对此一无所知,我会牢记在心。不幸的是,在这种情况下它是行不通的,因为从内存使用的角度来看,我确实有严格的要求,所以M应该很小,而且我猜会导致太多的精度损失。
gianluca 2010年

@gianluca:听起来您有1.大量数据,2.有限的内存资源,3.高精度要求。我知道为什么这个问题令您感到震惊!也许,正如@kwak所提到的,您可以计算其他一些价差度量:MAD,IQR,标准差。所有这些方法都可能适用于您的问题。
shabbychef

gianluca:>给我们更多关于内存大小,数组和所需精度的定量想法。不过,很可能您的问题将在@stackoverflow上得到最好的回答。
user603 2010年

4

过去,我使用以下方法来适度有效地计算绝对偏差(请注意,这是程序员的方法,而不是统计学家,因此,毫无疑问,可能会有像shabbychef这样的聪明技巧,可能会更有效)。

警告:这不是一个在线算法。它需要O(n)内存。此外,O(n)对于像这样的数据集[1, -2, 4, -8, 16, -32, ...](即与完全重新计算相同),其最坏情况下的性能为。[1]

但是,由于它在许多用例中仍然表现良好,因此可能值得在此处发布。例如,为了计算每件物品到达时-100至100之间的10000个随机数的绝对偏差,我的算法用了不到一秒钟的时间,而完整的重新计算花费了17秒以上(在我的机器上,每台机器和根据输入数据)。但是,您需要将整个向量保留在内存中,这对于某些用途可能是一个约束。该算法的概述如下:

  1. 与其使用单个矢量来存储过去的测量值,不如使用三个排序的优先级队列(类似于最小/最大堆)。这三个列表将输入分为三个部分:大于平均值的项,小于平均值的项和等于平均值​​的项。
  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
我缺少了一些东西:如果您必须“将整个向量保留在内存中”,这怎么算是“在线”算法?
ub

@whuber不,不丢失任何东西,我想这不是一个在线算法。它需要O(n)内存,在最坏的情况下,每添加一个项目就要花费O(n)时间。在正态分布的数据(可能还有其他分布)中,它的工作效率很高。
fmark

3

XXXss2/π


这是一个有趣的想法。您可以通过在线检测离群值来补充它,并在进行过程中使用这些值来修改估计值。
ub

您可能会使用Welford的方法在线计算我在第二个答案中记录的标准偏差。
fmark

1
但是,应该注意的是,这种方式可能会丢失诸如显式MAD之类的估计量的鲁棒性,这些估计量有时会促使其选择较简单的选择。
石英

2

MAD(x)只是两个并发的中值计算,每个中值计算都可以通过binmedian算法在线进行。

您可以在此处在线找到相关的论文以及C和FORTRAN代码。

(这只是在Shabbychef的巧妙技巧之上使用的一种巧妙技巧,以节省内存)。

附录:

存在许多用于计算分位数的较旧的多遍方法。一种流行的方法是维护/更新从流中随机选择的确定大小的观测值存储库,并在该存储库上递归计算分位数(请参阅评论)。这种(和相关的)方法已被以上建议的方法所取代。


您能否详细说明或参考MAD与两个中位数之间的关系?
石英

一世=1个ñ|X一世-一世=1个ñ|

嘿,实际上,我的意思是,如果您能解释一下这种关系如何使两个中位数同时出现;这些似乎取决于我,因为外部中位数的输入可能在内部计算的每个添加样本处都发生变化。您将如何并行执行它们?
石英

Ëd一世=1个ñX一世Xñ+1个Ëd一世=1个ñ+1个X一世ØñXñ+1个
user603 2013年

1

以下内容提供了一个不准确的近似值,尽管该不准确性将取决于输入数据的分布。它是一种在线算法,但仅近似于绝对偏差。它基于一种著名的在线计算方差算法,该算法Welford在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 

但是,根据您的用例,此误差幅度可能是可以接受的。

差异史


一世X一世一世X一世。您正在计算前者,而OP则需要后者。
shabbychef 2010年

我同意该方法不准确。但是,我不同意您对不精确性的诊断。韦尔福德的方差计算方法(甚至不包含sqrt)也具有类似的误差。但是,随着n变大,error/n变小很快就消失了。
fmark

韦尔福德的方法没有sqrt,因为它是在计算方差,而不是标准差。通过使用sqrt,似乎您正在估计标准偏差,而不是平均绝对偏差。我错过了什么吗?
shabbychef

@shabbychef Welfords的每次迭代都在计算新数据点对绝对偏差的贡献,平方。因此,我将每个贡献的平方根求平方,以得出绝对偏差。例如,您可能会注意到,在将差值加到偏差总和之前,我取了平方根,而不是像标准差那样取后。
fmark

3
我看到了问题;韦尔福德斯用这种方法掩盖了问题:使用均值的在线估计而不是均值的最终估计。尽管韦尔福德的方法对于方差是精确的(直到四舍五入),但该方法并非如此。问题不是由于sqrt不精确造成的。这是因为它使用了运行均值估计。要查看何时会中断,请尝试xs <- sort(rnorm(n.testitems)) 当我对您的代码进行尝试(将其修复为return之后a.dev / n)时,我得到的相对误差约为9%-16%。因此,此方法不是排列不变的,不会造成破坏……
shabbychef 2010年
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.