算法之美 之 小小方差增量算法帶來的大大收益


一個小小的方差增量算法,使得消除持續增長的上百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年的用戶數據,這個算法也不怕了。

從時間效率看:每次計算方差時,可以完全重用歷史樣本的指標,省了絕大部分基於樣本值的計算。

方差算法增量化后,無論空間還是時間效率,都得到了非常大的提升。

對於算法之美,小伙伴們,你們激動了嗎?

分類 算法之美

優雅程序員 原創 轉載請注明出處

圖片二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM