項目在統計UV/PV時用到了Druid的Hyper hyperunique算法,書上介紹這種算法求出的UV/PV存在一定誤差,因此需要了解下誤差來自哪里。
實現去重功能,最簡單的就是使用set記錄集合本身,缺點與前面Bloom Filter差不多,顯而易見,需要大量內存空間。HyperLogLog為解決這個問題而生。
另外redis也實現了HyperLogLog的結構,所以可以從redis源碼上分析下其實現。
1。基數計數
基數是指一個集合中不同元素的個數。假設有一組數據{1, 2, 3, 3, 4, 5},除去重復的數字之后,該組數據中不同的數有5個,則該組數據的基數為5。
那什么是基數統計呢?基數統計是指在誤差允許的情況下估算出一組數據的基數。
2。伯努利過程
投擲一次硬幣出現正、反兩面的概率均為1/2。如果我們不斷的投擲硬幣,直到出現一次正面,在這樣的一個過程中,投擲一次得到正面的概率為1/2,投擲兩次才得到正面的概率為1/2^2….依次類推,投擲k次才得到一次正面的概率為1/2^k。這個過程在統計學上稱為伯努利問題。
思考下面兩個問題:
-
進行n次伯努利過程,所有投擲次數都小於k的概率
-
進行n次伯努利過程,所有投擲次數都大於k的概率
針對第一個問題,在一次伯努利過程中,投擲次數大於k的概率為1/2^k,也就是投了k次反面的概率。因此,在一次過程中投擲次數不大於k的概率為1-1/2^k。因此n次伯努利過程所有投擲次數都不大於k的概率為
很顯然,第二個問題,n次伯努利過程,所有投擲次數都不小於k的概率為
從上述公式中可得出結論:當n遠小於2^k時,P(x>=k)幾乎為0,即所有投擲次數都小於k;當n遠大於2^k時,P(x <= k)幾乎為0,即所有投擲次數都大於k。因此,當x=k的情況下,我們可以把2^k當成n的一個粗糙估計。
3。基數統計和HyperLogLog算法
將上述伯努利過程轉換到比特位串上,假設我們有8位比特位串,每一位上出現0或者1的概率均為1/2,投擲k次才得到一次正面的過程可以理解為第k位上出現第一個1的過程。
那么針對一個數據集來說,我們用某種變換將其轉換成一個比特子串,就可以根據上述理論來估算出該數據集的技術。例如數據集轉換成00001111,第一次出現1的位置為4,那么該數據集的基數為16。
於是現在的問題就是如何將數據集轉換成一個比特位串?很明顯,哈希變換可以幫助我們解決這個問題。
選取一個哈希函數,該函數滿足一下條件:
-
具有很好的均勻性,無論原始數據集分布如何,其哈希值幾乎服從均勻分布。這就保證了伯努利過程中的概率均為1/2
-
碰撞幾乎忽略不計,也就是說,對於不同的原始值,其哈希結果相同的概率幾乎為0
-
哈希得出的結果比特位數是固定的。
有了以上這些條件,就可以保證“伯努利過程”的隨機性和均勻分布了。
接下來,對於某個數據集,其基數為n,將其中的每一個元素都進行上述的哈希變換,這樣就得到了一組固定長度的比特位串,設f(i)為第i個元素比特位上第一次出現”1“的位置,簡單的取其最大值f_max為f(i)的最大值,這樣,我們就可以得出以下結論:
-
當n遠小於2^f_max時,f_max為當前值的概率為0
-
當n遠大於2^f_max時,f_max為當前值的概率為0
這樣一來,我們就可以將f_max作為n的一個粗糙估計。當然,在實際應用中,由於數據存在偶然性,會導致估計量誤差較大,這時候需要采用分組估計來消除誤差,並且進行偏差修正。
HyperLogLog算法原理通俗版:Here I’ll cover only the basic idea using a very clever example found at [3]. Imagine you tell me you spent your day flipping a coin, counting how many times you encountered a non interrupted run of heads. If you tell me that the maximum run was of 3 heads, I can imagine that you did not really flipped the coin a lot of times. If instead your longest run was 13, you probably spent a lot of time flipping the coin.
所謂分組估計就是,每一個數據進行hash之后存放在不同的桶中,然后計算每一個桶的f_max,最后對這些值求一個平均f_avg,即可得到基數的粗糙估計2^f_avg。
誤差消減原理通俗版:However if you get lucky and the first time you get 10 heads, an event that is unlikely but possible, and then stop flipping your coin, I’ll provide you a very wrong approximation of the time you spent flipping the coin. So I may ask you to repeat the experiment, but this time using 10 coins, and 10 different piece of papers, one per coin, where you record the longest run of heads. This time since I can observe more data, my estimation will be better.
實際實現中,分組估計也不能很好的解決誤差問題,還需要額外的偏差修正工作。
http://algo.inria.fr/flajolet/Publications/DuFl03-LNCS.pdf
4。Redis實現
-
對於輸入,計算64位hash
-
hash最右14bit來進行分桶,桶的個數是2 ^14=16384個
-
統計hash左側50bit,第一次1出現的位置(用bit存儲需要6bit:2^6 = 64 > 50)
-
與對應桶的6bit值比較,若大於原來值,則更新之
redis HyperLogLog內存占用:6(存儲第一次出現位置) * 16384(桶個數) / 8 (1byte = 8bit)/ 1024(bytes) = 12K
因此redis使用12K固定大小內存即可完成對一個Key的Unique統計,空間效率極高。