算法之美 之 小小方差增量算法带来的大大收益

一个小小的方差增量算法,使得消除持续增长的上百GB的明细数据成为可能,空间效率和时间效率都可得到无以伦比的提升。

下面一码给你重现整个过程,小伙伴们一起激动激动。

背景

搞推荐就要玩好私人定制,要玩好私人定制,就得分析用户的购买和浏览行为。我们系统里某个地方就需要针对每个用户,计算他(她)曾经购买过的所有产品的价格的方差。

来,和你一起回顾下方差的定义。

方差的统计学定义

方差是反应数值型数据离散程度的最重要的指标。

假设X样本的有N个样本值:

\[x_1, x_2, ... , x_N \]

X样本的平均值计算很简单:

\[\overline{X} = \frac 1 N \sum_{i=1}^N x_i \]

那么计算X样本的方差的公式如下:

\[\sigma_X^2 = \frac 1 N \sum_{i=1}^N (x_i - \overline{X})^2 \]

从表面上看,为了计算一组样本值的方差,需要知道所有样本值明细。

持续增长中的上百GB的明细数据

为了保证及时算出产品价格方差这一重要指标,专门存储了每个用户购买的所有产品的价格,还没到一年,数据量就奔着百GB俱乐部的规模去了。问题来了,如果需要分析更长时间内用户的数据,5年,10年,这数据就上TB。总是有增无减,就不是可持续发展的套路,这个算法套路得改改。

如果方差算法能够像订单数一样不断增量处理,不就万事大吉了吗?

增量方差的推导

假设我们现在有两组样本值,一组为历史样本值:

\[h_1, h_2, ... , h_M \]

一组为增量样本值:

\[a_1, a_2, ... , a_N \]

根据之前介绍的方差和均值的定义,我们可以得到两组样本值的如下四个指标:

历史平均值

\[\overline{H} = \frac 1 M \sum_{i=1}^M h_i \]

历史方差

\[\sigma_H^2 = \frac 1 M \sum_{i=1}^M (h_i - \overline{H})^2 \]

增量样本均值

\[\overline{A} = \frac 1 N \sum_{j=1}^N a_j \]

增量样本方差

\[\sigma_A^2 = \frac 1 N \sum_{j=1}^N (a_j - \overline{A})^2 \]

目前关键问题在于:

\[h_1, h_2, ... , h_M, a_1, a_2, ... , a_N \]

这组全量样本值的方差是否能够由历史样本和增量样本的指标直接计算得到。下面一码就给你推导推导,看能够做到这点。

首先,全量样本均值的计算如下:

\[\begin{align} \overline{X} &= \frac 1 {M + N} \left[ \sum_{i=1}^M h_i + \sum_{j=1}^N a_j \right] \nonumber \\\\ &= \frac { M\overline{H} + N\overline{A}} {M + N} \nonumber \end{align} \]

其次,全量样本方差的计算和推导如下:

\[\begin{align} \sigma^2 &= \frac 1 {M + N} \left[\sum_{i=1}^M \left(h_i - \overline{X}\right)^2 + \sum_{j=1}^N \left(a_j - \overline{X}\right)^2 \right] \nonumber \\\\ &= \frac 1 {M + N} \left[ \sum_{i=1}^M \left((h_i - \overline{H}) - (\overline{X} - \overline{H})\right)^2 + \sum_{j=1}^N \left((a_j - \overline{A}) - (\overline{X} - \overline{A})\right)^2 \right] \nonumber \\\\ &= \frac 1 {M + N} [ \sum_{i=1}^M \left((h_i - \overline{H})^2 - 2(h_i - \overline{H})(\overline{X} - \overline{H}) + (\overline{X} - \overline{H})^2\right) \nonumber \\\\ & + \sum_{j=1}^N \left((a_j - \overline{A})^2 - 2(a_j - \overline{A})(\overline{X} - \overline{A}) + (\overline{X} - \overline{A})^2\right) ] \nonumber \\\\ &= \frac 1 {M + N} [ M\sigma_H^2 + M(\overline{X} - \overline{H})^2 - 2(\overline{X} - \overline{H})(\sum_{i=1}^M h_i - M\overline{H}) \nonumber \\\\ &+ N\sigma_A^2 + N(\overline{X} - \overline{A})^2 - 2(\overline{X} - \overline{A})(\sum_{j=1}^N a_j - N\overline{A}) ] \nonumber \\\\ &= \frac 1 {M + N} \left[ M\sigma_H^2 + M(\overline{X} - \overline{H})^2 + N\sigma_A^2 + N(\overline{X} - \overline{A})^2 \right] \nonumber \\\\ &= \frac { M\left[\sigma_H^2 + \left(\overline{X} - \overline{H}\right)^2\right] + N\left[\sigma_A^2 + \left(\overline{X} - \overline{A}\right)^2\right] } {M + N} \nonumber \end{align} \]

从推导出来的公式看,通过两组样本的样本数,均值,方差,完全可以计算出全量样本的方差。

增量方差的实现

毕竟推演公式是尘封多年的技能,还是通过代码验证才能让一码放心。

case class Measures(n: Int, sum: Double, variance: Double) {
  def avg = sum / n

  def appendDelta(delta: Measures): Measures = {
    val newN = this.n + delta.n
    val newSum = this.sum + delta.sum
    val newAvg = newSum / newN

    def partial(m: Measures): Double = {
      val deltaAvg = newAvg - m.avg
      m.n * ( m.variance + deltaAvg * deltaAvg )
    }

    val newVariance = (partial(this) + partial(delta)) / newN

    Measures(newN, newSum, newVariance)
  }
}

Measures包含了样本数,均值,和以及方差,构成了可增量计算方差的要素。同时也用它承载职责“方差增量算法”。

case class Samples(values: Seq[Double]) {
  def measures: Measures = {
    if (values == null || values.isEmpty)
      Measures(0, 0d, 0d)
    else
      Measures(values.length, values.sum, variance)
  }

  private def variance: Double = {
    val n = values.length
    val avg = values.sum / n
    values.foldLeft(0d) { case (sum, sample) =>
      sum + (sample - avg) * (sample - avg)
    } / n
  }
}

Samples解决了如何计算一组样本值所需要的统计指标,按统计学定义直接计算,无增量算法。

object DeltaVarianceUtils {
  def main(args: Array[String]): Unit = {
    implicit val arrayToSamples = (values: Array[Double]) => Samples(values)

    val historicalSamples = Array(1.5d, 3.4d, 7.8d, 11.6d)
    val deltaSamples = Array(9.4d, 4.2d, 35.6d, 77.9d)

    println("Variance: "
      + (historicalSamples ++ deltaSamples).measures.variance
    )
    println("Variance calculated by delta algorithm: "
      + historicalSamples.measures.appendDelta(deltaSamples.measures).variance
    )
  }
}

第一种是通过传统的统计学定义直接计算方差。先把历史样本值和增量样本值合并,然后计算方差。

第二种是增量化的方差算法。把历史样本值转换成Measures,也就是可增量计算方差的要素,然后将增量样本值对应的measures要素合并进来,得到最终的方差。

运行结果如下:

Variance: 598.2168750000002
Variance calculated by delta algorithm: 598.2168750000001

大大的收益

方差算法增量化后,从空间效率看:每个用户不需要再存任何产品价格明细,只需要存Measures中的三要素(历史样本数,历史价格总和,历史样本方差)即可,需要记录的数据量对每个用户是恒定的,哪怕是需要分析100年的用户数据,这个算法也不怕了。

从时间效率看:每次计算方差时,可以完全重用历史样本的指标,省了绝大部分基于样本值的计算。

方差算法增量化后,无论空间还是时间效率,都得到了非常大的提升。

对于算法之美,小伙伴们,你们激动了吗?

分类 算法之美

优雅程序员 原创 转载请注明出处

图片二维码

关注微信公众号 YoyaProgrammer,把经典系列 大数据处理/大话重构/算法之美/时间管理 装进口袋,随时翻看。
posted @ 2015-07-06 07:24  一码  阅读(6711)  评论(8编辑  收藏  举报