最近做UV近似统计的需求,整理了UV统计相关的算法,Cardinality Estimation的相关研究可以用于UV近似统计。下面所列出算法重在实现逻辑,相关证明过程可以参考对应论文。我们的UV统计采用了HyperLogLog算法,因此会对HyperLogLog算法做详细介绍。
首先,Linux的sort命令和SQL语言中的distinct关键字可以实现UV统计,如下
sort -u log.txt | wc -l
SELECT
DATE_TRUNC(‘day’,event_time),
COUNT(DISTINCT user_id),
COUNT(DISTINCT url)
FROM weblog
部分UV统计,数据量大,对实时性要求高,这就要求统计程序要尽可能的降低时空复杂度。
和PV不同,UV无法直接进行时间或其他维度上的合并,例如,有前一分钟和当前时刻的UV,要计算这两分钟的UV,不能直接将结果合并。
HashSet有去重功能,所以使用HashSet即可实现UV统计,Java的代码如下
public static long count(Iterable<String> stream) {
HashSet<String> hset = new HashSet<>();
for (String x : stream)
hset.add(x);
return hset.size();
}
HashSet使用对象存储,内存开销大,仅适用于小数据量计算。
统计UV时,使用位图存储用户信息,而不是HashSet的对象,极大节省内存空间,Java中的位图实现是java.util.BitSet,如下代码,创建长度为1024的位图,第十位设置为true,返回uv
BitSet bset = new BitSet(1024);
bset.set(10);
int uv = bset.cardinality();
2^32≈42亿,2^32bit≈537M |
从上图可以看出,只需约537M的空间就可统计42亿用户的UV,但是我们如果要多维度统计UV,就需要多个位图结构,所需空间是n * 537M,维度较多时内存开销还是很大。
Bitmap在处理非数值类型时,需要使用hash函数将其转换为数值,此过程可能产生hash冲突,使得结果出现误差,hash函数选择较为重要。
此外,极端情况下,若Bitmap处理的数据仅有少量的大树时,仍然要申请足够长度的bit,造成浪费。
那么有没有比位图省空间,并且还是准确计算UV的算法吗,目前是没有的,接下来介绍的都是估计算法,用精度换空间。
Linear Probabilistic Counter是基于Bitmap的变形。
该算法分为两部分
和前面介绍的Bitmap相比,Linear算法的位图长度可以小于已知的基数(例如UV统计,已知最大Uid,Bitmap的位长度要cover住Uid,而Linear则不需要),起到了减小了内存开销的作用。
位图长度可以小于已知的基数,就会有冲突的情况,hash函数的选择变得重要,合适的hash函数能够有效降低hash冲突,同时,m的大小也决定着hash冲突的频率,显然m越大hash冲突的概率就越小,计算结果也更精确。
probabilistic counting algorithms for database applications算法流程如上
见HyperLogLog
见HyperLogLog
介绍了几种用于计算UV的算法,PC和LPC算法只讨论了实现过程,数学证明和偏差计算参考给出的论文连接,LogLog和HyperLogLog算法下一篇详细介绍。
参考:
1.Cardinality Estimation (princeton)
2.A Linear-Time Probabilistic Counting Algorithm for Database Applications
3.loglog counting of large cardinalities
4.Big Data Counting: How To Count A Billion Distinct Objects Using Only 1.5Kb Of Memory
5.probabilistic counting algorithms for database applications