简单介绍留存率的概念,说明数仓建设中对留存率计算的优化思路
在互联网行业中,用户在某段时间内开始使用应用,经过一段时间后,仍然继续使用该应用的用户,被认作是留存用户。
留存率就是留存用户与全部用户的比值,计算公式
留存率 = 留存用户数 / 用户数 * 100%
比如昨天来了100个人,今天这100个人里面的60个人又来了,那么留存率就是60%。
留存率反应了一个产品的用户黏性,留存越高说明用户在使用这个产品之后,继续使用的概率越大。
在用户运营越来越重要的今天,留存率作为公司的重要指标,也越来越被重视起来。
留存率有很多计算口径,适用于不同的分析场景。但都是要确定两个时间窗口,第一个时间段用来圈人,第二个时间段用来观察被圈的人有没有再次访问。一般来说看的比较多的有如下几种口径
口径名称 | 前一时间段 | 下一时间段 |
---|---|---|
次日留存 | 1天 | 1天 |
次三日留存 | 1天 | 3天 |
次七日留存 | 1天 | 7天 |
次30日留存 | 1天 | 30天 |
周留存 | 7天 | 7天 |
月留存 | 30天 | 30天 |
自然月留存 | 上个自然月 | 下个自然月 |
此外游戏产品还很看重用户注册之后的第一日留存、第二日留存…一直到第7日留存,含义是第0天来的人,在第1天、第2天…第7天的留存,是一个不断下降的曲线,游戏策划的一大目标就是让这条曲线下降变慢一点。
可以看到留存率的计算口径众多,时间跨度广,如果再摊上用户数量巨大,计算起来就是一件费时费力的工作了。
简单来说就是没有统一计算的底层留存表,数据开发者在需要计算留存的时候,自己把两块用户数据left join起来,然后计算留存。
这样好处是简单快速;缺点是有数据口径不统一的隐患,逻辑很难复用。只适用于初级的数仓报表建设。因为这种没有任何设计思路,这里就不再展开。
思路是很简单的,如果我们知道了每一个用户,在前一段时间的访问天数M,在后一段时间的访问天数N,那么留存率就是 sum(M>0 and N>0) / sum(M > 0) * 100%
而我们一般用到的时间段是可以枚举的:1、2、3、4、5、6、7、14、28、29、30、31。那么,如果我们有一张表包含了12 * 2 = 24
个字段(前后要*2),就能方便计算留存率了。
但是,这种方法会遇到一个问题,就是未来是不知道的,在可以计算次日留存的时候,月留存还无法得到结果,总不能等到一个月之后才拿到次日留存吧。所以就遇到需要回刷的问题。在每一个时间点都需要回刷一次,假设关注12个时间点,一张表就相当于12张表的计算量。虽然看起来计算量大了,但是如果很多地方都需要用到,比起粗放的前一种方式还是提升了很多的。
这是本篇文章主要想讲述的内容,它继承了上面保存用户访问情况的思想,计算量不会太大,口径也可以更加灵活。
假设我们可以把用户两个月的访问情况都保存在表里,1就是来了,0就是没来,两个月算62天,一天一个字段,就是62列。那么这份数据就可以完全覆盖上面从次日留存到月留存,不需要回刷。为什么呢,因为在最新日期的数据里面,可以看到用户在过去60天,每一天的访问情况,假设用户在60~30这30个日期来访是M,30~1这30个日期来访是N,那么依然可以通过公式sum(M>0 and N>0) / sum(M > 0) * 100%
计算留存。
有人觉得62个字段太多了,放在一个表里不值得,这个完全是可以优化的,一个bigint有64位,64 - 62 = 2
还能剩余两位。所以我们只需要一个bigint,理论上就可以计算绝大多数的留存口径。
在这里先做一个约定,0代表没来,1代表来,从右到左第1到第62位,分别代表距今天0到61天,用户是否来访。
那么我们就可以定义一个UDF来计算任意口径用户来访天数,假设叫get_range, 这个函数需要4个信息
简单示例如下
public int evaluate(final bigint info, final String baseDate, final String targetDate, int range) {
int r = 0;
int diff = datediff(baseDate, targetDate);
if (diff >= 1 && diff <= 62 && range > 0){ // 保证没有越界
for (int i = 0; i < range && i <= diff; i++){
r += ((info >> (diff - i)) & 1);
}
}
return r;
}
以计算次日留存为例, 就可以
get_range(info, '20190901', '20190831', 1) as M,
get_range(info, '20190901', '20190901', 1) as N
然后 sum(M>0 and N>0) / sum(M > 0) * 100%
来计算留存了
总结一下,通过一个bigint来存储用户的访问情况,可以非常方便的计算用户的留存,但是也需要很好的工具链来保障这个bigint生成的正确性,对软实力要求很高。
如果不追求极致的性能,使用回刷的方式也可以达到方便计算的目的。最不推荐的就是在各个用到地方算一次留存,虽然看起来简单,但是最后维护最麻烦,使用的资源也随着时间越来越多。