多维可聚合UV算法

79 阅读9分钟

9-10双月的用户数等于9月和10月用户数的累加吗?

在日常的大数据开发工作中,不同维度条件下的uv等QoE指标的聚合与拆分需要耗费大量的计算与存储资源。

背景

QoS与QoE指标的计算方法差异

维度:数据不同的属性,如时间、地区、场景、编码格式等

指标:数据的具体量级表征,如播放量、播放时长、用户数等

粒度:根据维度对指标进行拆分,拆分的维度越多,数据的粒度越细,比如可以把播放量拆app_id、os、编码方式等等

聚合:把细粒度的指标汇总成粗粒度的指标:比如把24个小时粒度的指标汇总,得到了天粒度的指标。拆分的含义则相反。

在数据建设过程中,我们常常会遇到两类指标:QoS(Quality of Service)与QoE(Quality of Experience)指标。其中QoS注重服务质量的衡量,比如播放量、播放时长;而QoE指标注重用户体验指标的衡量,比如用户量、人均播放时长等。这两类指标不仅在意义上有差别,在计算方式上也不同。

QoS指标:不同维度下的拆分与聚合只需简单的数学累加。

QoE指标:不同维度下的拆分与聚合不能依靠简单的累加。

遇到的问题

不同维度下的QoE指标无法进行拆分与聚合,所以每次为了获得不同维度下的QoE指标,均需要从明细中获取。这意味着大量的计算与存储资源消耗

  • QoE大盘指标建设的问题

    • QoE指标常常需要在app_id、os、tag这三个维度下拆分与聚合分析,因此需要分别通过明细去计算着3个维度不同组合情况下的QoE指标。
  • xxx数据建设的问题

    • xxx需要对客户的QoS和QoE指标进行展示。QoS指标可以通过聚合后的指标进行累加得到,而QoE指标必须通过明细计算,因此需要维护两套数据流以产出QoS和QoE数据:计算与存储资源浪费大,查询效率低。

传统的数据结构与算法随着数据量级的增加,对应的空间复杂度与时间复杂度均爆炸增长,无法满足大数据场景下不同维度QoE指标的灵活聚合与拆分。

HLL算法的原理简介

为了解决上述问题,我们探索了以下方法。

bitmap

bitmap可以理解为通过一个bit数组来存储特定数据的一种数据结构,bit是数据的最小存储单位,每一个bit位都能独立包含信息,因此能大量节省空间。如果定义一个很大的bit数组,基数统计中每一个元素对应到bit数组的其中一位,例如8位的bit数组 00110101代表实际数组[2,3,5,7]。每新加入一个元素,只需要将已有的bit数组和新加入的数字做按位或 (or)计算。

bitmap中1的数量就是集合的基数值。

  • 存储成本

如果要统计1亿个数据的基数值,大约需要内存:100000000/8/1024/1024 ≈ 12M

  • 合并性能

bitmap有一个很明显的优势,就是可以轻松合并多个统计结果,只需要对多个结果进行或运算即可。

  • 存在的问题

bitmap对于内存的节约量是显而易见的,但还是不够。比如统计一个链接的访问UV需要12M,如果对10000个链接进行统计,就需要将近120G了,同样不能广泛用于大数据场景。

在不熟悉数据规模的时候,如何设置BitMap的大小也是一个问题。

Linear Counting

是一种早期的基数估计算法

首先定义一个hash函数:hash(x): -> [0,1,2,…,m-1],假设该hash函数的hash结果服从均匀分布。

接着定义一个长度为m的bit数组,开始每一位上都初始化为0。

然后对集合中的每个元素进行hash得到k,如果bitmap[k]为0则置1。

最后统计bitmap数组里为0的位数u。

设集合基数为n,则有:n^=mln(u/m)\hat n =−m*ln(u/m),且n^\hat n其为的最大似然估计。n^=mln(um\hat n =−m*ln(\frac u m

集合中共有12个元素,hash函数映射到[0,7]中(m=8)。如图hash结果后共有2个bit位为0,即u=2。代入上述公式可得估计结果为11.1(实际值为12)。

为了避免当m设置太小,出现u=0,n̂ 为无穷大的情况,合理的m设置十分关键

  • 存储成本

LC在空间复杂度方面并不算优秀。

可以看出精度要求越高,则bitmap的长度越大。可以看出对于N是几万以内是很有优势的。但是,随着m和n的增大,m大约为n的十分之一。因此,LC所需要的空间只有传统的bitmap直接映射方法的1/10,但是从渐进复杂性的角度看,空间复杂度仍为O(Nmax);

  • 合并性能

Linear Counting非常方便于合并,直接通过按位或的方式即可。

LogLog Counting & HyperLogLog Counting

抛硬币回合次数的估计

硬币拥有正(1)反(0)两面,每一次上抛至落下,最终出现正反面的概率都是1/2。

那么需要多少回合才能出现0001这样的记录呢?通过概率计算可知,这样的概率为(1/2)^4=1/16。也就是说,平均需要16回合,才会出现0001这样的序列。换言之,如果出现了0001这样的序列,可以期望已经进行了16回合的抛硬币。

进一步地,目前出现了这样一个序列:w=w1w2w3...wk...wn,其中wk=1,w1w2w3...wk-1均为0,不考虑wk+1~wn,则出现这样的序列其概率为(1/2)^k,则一共进行了2^k回合。

分桶减小误差

受到抛硬币回合次数估计的启发,对UV统计有了一个粗略的设想:将visitor_id进行hash函数处理成均匀分布的的01序列,找到序列第一次出现1的位数k,则该序列出现的概率为(1/2)^k,总的UV数量(抛硬币的回合数)为2^k。同样对其他的visitor_id进行处理,获得序列第一次出现1的位数k,最终比较所有k的大小,记录kmax,则估算总UV为2^kmax。

这种算法看起来有一定的道理,但是忽略了极端情况所带来的影响:仍然以抛硬币为例,可能只需要1个回合就可以抛出0001序列,与估算的16回合相差甚远。为了减小这种极端情况带来的误差,进行了分桶策略。

已知这样一个序列:x=xn...xb+2xb+1xb...x3x2x1,首先依据低位xb...x3x2x1对x进行分桶,再根据高位xn...xb+2xb+1计算k值估算基数,最终每个分桶中均会得到一个估计的基数值,进一步取得均值作为最终的基数值。

当分桶数量为m时,基数估计为:

\hat n=m*2^{{\frac 1 m}{\sum{max(k_{i})}}

仍然以1个回合抛出0001序列为例,在不同的分桶数下,估算的回合数也发生变化。

1个桶->16回合,

2个桶->8回合,

4个桶->8回合,

也就是说,增加桶数在一定条件下可以有效提高基数的估算精度。

通过数学分析进一步修正公式,增加修正系数αm,在不同的分桶数量下选择合适的αm对结果进行修正:

\hat n=α_m *m* 2^{{\frac 1 m}{\sum{max(k_{i})}}

修正系数αm\alpha_m常常采取以下近似值:

αm={0.673,for m=160.697,for m=320.709,for m=640.72131+1.079m,for m=128\alpha_m = \begin{cases} 0.673 , \text{for } m = 16\\ 0.697 , \text{for } m = 32\\ 0.709 , \text{for } m = 64\\ \dfrac{0.7213}{1 + \dfrac{1.079}{m}} , \text{for } m =\ge 128\\ \end{cases}

以上即为LogLog Counting算法的基本思路与实现,标准误差为:

StdError1.03mStdError ≈ \frac {1.03} {\sqrt{m}}

当m=1024时,误差约为4%。

存储成本

空间复杂度与Hash 长度L分桶数m有关。

  • 设基数空间n ->n=2^->L=log2L=log_2
  • max(ki)max(k_i) \le -> 每个桶需要p=log2p=log_2bit的内存 -> 每个桶需要p=log2(log2np=log_2(log_2nbit内存
  • 桶数m -> m*p -> 一共需要 mlog2(log2nm*log_2(log_2nbit的内存大小。

可以看到,LLC算法需要的空间仅仅是基数空间n的两次log的大小。这也是loglog Counting算法的命名来源。

假设n为40亿,m为1024,L值为32bit,则每个桶需要5bit空间存储,一共需要5×1024 = 5120 bit = 640字节。

合并性能

合并操作非常简单,只需要比较多个相同桶编号的k值并取最大者为合并后此桶的k值,再进行计算即可。

以上为LogLog Counting算法的介绍。

优化为HyperLogLog Counting

  • 使用调和平均代替几何平均数
  • 分段偏差修正

HLLC中,为了解决LLC在基数较小时偏差大的问题,在小基数时,选择LC估计。设E为估计值:

  • E52E \le \frac 5 2时,使用LC进行估计。
  • \frac 5 2m \le E \le {\frac 1 {30} 2^{32}时,使用HLLC公式计算。
  • 130232{\frac 1 {30} 2^{32}} \le 时,估计公式为n^=232lg(1E232\hat n=-2^{32} lg(1- {\frac E {2^{32}}}

工程实践与成果

实现架构

Hive:一种低成本、高延迟的存储介质,用于存储大量的历史数据

ClickHouse:一种高成本、低延迟的存储介质,用于存储少量的热数据

使用方法

解决的问题与取得的成果

根据上述架构方案,解决了前述的问题。

总结

在大数据场景下,采用传统的数据结构对UV进行多维度聚合及统计的时间、空间代价极大,而我们采用了具有查询速度快、占用内存小、合并效率高、估算精度准等特点的HyperLogLog算法,大量节约了时间与空间成本。

参考文档

Loglog counting of LARGE Cardinalities

HyperLogLog: The analysis of a near-optimal cardinality estimation algorithm

大数据算法:基数统计

神奇的HyperLogLog算法

CodingLabs - 五种常用基数估计算法效果实验及实践建议

基数估计算法(二):Linear Counting算法