作者:史少锋,Apache Kylin committer & PMC,2019/10/11
在上次文章《如何在 1 秒内做到大数据精准去重》中,我们介绍了 Apache Kylin 为什么要支持大数据集上的精确去重,以及基于 Bitmap 的精确去重原理等。看到今天的文章标题,你肯定要问,精确去重跟用户行为分析又能有什么关系呢?原来啊,Kylin 采用 Bitmap 完整记录了每个维度组合下的用户集合,利用 Bitmap 提供的或(or)运算方法来高效精准地回答了各种条件下的去重用户数。其实 Bitmap 除了支持或(or)运算外,还支持与(and)运算。因此,稍加扩展,Kylin 就可以基于 Bitmap 的中间结果,轻松实现诸如留存、漏斗等大量使用交集运算的分析,从而非常方便地运用在用户行为、用户画像等领域中。可以说精确去重功能有着一石两鸟的价值,本文将为您介绍如何使用 Kylin 来实现精准的用户行为分析。
先从一个简单的例子说起吧。现在有一个 app 的用户访问记录表 access_log,它包含三个字段:DT (访问日期),User ID(用户标示)和 Page(访问页):
△ 表 1:样例数据原始表
在 Kylin 里创建一个模型,选择 DT 和 Page 做维度,User ID 做度量;
然后创建一个 Cube,使用 DT 和 page 做维度,定义 count (distinct user_id) 度量且使用 Bitmap 为数据类型(关于 Kylin 的使用,参考 Apache Kylin 官网教程)。
这个 Cube 构建完成后,对于只包含 DT 维度的 Cuboid,它的数据结构如下:
△ 表 2:样例 Cube 1
注:上面的用户集合“User IDs”用的是易于阅读的数组格式,在实际存储中使用 Bitmap(位图)格式,也就是一组 0 或 1 的 bit 数组,如 [100,101,102] 就在第 100-102 位放 1,其它位放 0 代表。因此,Bitmap 不但非常省空间,而且非常适合计算机做交并集运算。
如果要计算某天或某几天的 UV,SQL 如下:
select dt, count(distinct user_id) from access_log where dt >&#61; &#39;20190101&#39; and dt <&#61; "20190103" group by dt
根据查询 Bitmap 的 Cardinality&#xff08;基数&#xff09;&#xff0c;获取到 UV 值&#xff1a;
△ 表 3&#xff1a;样例查询结果 1
对于 App 的运营者来说&#xff0c;留存分析是一种常见的分析手段&#xff0c;常用于提升用户留存率&#xff0c;它的主要目标是找到影响用户留存的关键因素。因为获取用户是有一定成本的&#xff0c;如果新获取的用户大部分都留不住&#xff0c;那么拉新的投入产出比就会很低。通常会分析日留存、周留存和月留存。
以日留存为例&#xff0c;要计算第一天访问的用户中&#xff0c;有多少在第二天、第三天继续访问了 app。如果使用 HiveQL或 Spark SQL 来计算第一天和第二天的留存用户数&#xff0c;写法大致如下&#xff1a;
SELECT count(distinct first_day.USER_ID) FROM(select distinct USER_ID as USER_ID from access_log where DT &#61; &#39;20190101&#39;) as first_day INNER JOIN(select distinct USER_ID as USER_ID from access_log where DT &#61; &#39;20190102&#39;) as second_day ON first_day.USER_ID &#61; second_day.USER_ID
可以看出&#xff0c;使用 Hive/Spark 计算留存用户&#xff0c;需要先写多个子查询&#xff0c;分别计算出各日的用户集合&#xff0c;然后通过 inner join 的方式实现交集计算&#xff0c;最后在外层再做 count(distinct&#xff09;运算。在数据量很大的时候&#xff0c;这样的多个子查询的join会非常慢且容易内存溢出。
大家可以看到&#xff0c;其实拿这几日访问用户的 bitmap&#xff0c;相互做与&#xff08;and&#xff09;操作&#xff0c;就可以高效地得到留存数字&#xff0c;如“第一天”and“第二天”访问的用户&#xff1b;这样跟第一天的访问用户数做个比较&#xff0c;就可以计算出第二天留存率等。
为了便于在 SQL 中做“与”操作&#xff0c;Kylin 提供了一个自定义函数&#xff1a;“intersect_count”&#xff08;详见文末“参考”文章【2】&#xff09;。顾名思义&#xff0c;就是做交集以后的结果数。该函数用法如下&#xff1a;
intersect_count(columnToCount, columnToFilter, filterValueList)
其中&#xff1a;
&#96;columnToCount&#96; 要做计数的列&#xff0c;也就是bitmap存储的列&#xff0c;这里就是“user_id“
&#96;columnToFilter&#96; 做交集计算的列&#xff0c;如果是把多个不同日期的bitmap来做交集&#xff0c;那么这列就是日期&#xff1b;
&#96;filterValueList&#96; 做交集计算的bitmap的key值&#xff0c;需要是一个数组。
例如下面的 SQL 查询了’20190101′ 与 ‘20190102’这两天的交集用户数&#xff1a;
select intersect_count(user_id, dt, array[&#39;20190101&#39;, &#39;20190102&#39;])
from access_log
where dt in (&#39;20190101&#39;, &#39;20190102&#39;)
可以看出&#xff0c;使用 Kylin&#xff0c;一个 intersect_count 函数就可以完成多日用户集合的交集计算。这里where条件中也进行了日期的筛查&#xff0c;为的是减少从底层存储加载的数据。显然&#xff0c;相比于Hive 和 Spark SQL&#xff0c;Kylin 的用法更加简单明了&#xff0c;而且基于预计算的 Bitmap 做交集&#xff0c;比现场 join 效率高很多&#xff0c;可以做到秒级响应。
通过 Kylin 还可以很方便地在一条 SQL 中查询多日的 UV 及留存&#xff0c;避免反复查询&#xff0c;如&#xff1a;
select city, version,
intersect_count(user_id, dt, array[&#39;20161014&#39;]) as first_day_uv,
intersect_count(user_id, dt, array[&#39;20161015&#39;]) as second_day_uv,
intersect_count(user_id, dt, array[&#39;20161016&#39;]) as third_day_uv,
intersect_count(user_id, dt, array[&#39;20161014&#39;, &#39;20161015&#39;]) as retention_oneday,
intersect_count(user_id, dt, array[&#39;20161014&#39;, &#39;20161016&#39;]) as retention_twoday
from access_log
where dt in (&#39;2016104&#39;, &#39;20161015&#39;, &#39;20161016&#39;)
group by city, version
order by city, version
同理&#xff0c;如果 Cube 中有周、月做维度&#xff0c;那么这里把日期换成周、月就可以很方便地计算周留存、月留存了。
漏斗分析&#xff0c;又叫转化漏斗&#xff0c;就是将一个特定过程的多个步骤间的转化情况&#xff0c;以漏斗的形式展示出来&#xff0c;通过图形直观地发现流失最严重的环节&#xff0c;从而有针对性地去进行优化。
△ 表1&#xff1a;漏斗分析
可以看出&#xff0c;漏斗分析中也要用到交集运算&#xff0c;例如&#xff1a;有多少访问了首页的用户&#xff0c;进入到了产品详细页&#xff1f;看了产品详情页的&#xff0c;有多少用户将它加入到了购物车&#xff1f;产品运营人员非常关心这些指标&#xff0c;因为它代表了用户在使用中每一步的转化关系&#xff1b;如果某一个路径上的转化率较低&#xff0c;意味着潜在的问题和风险&#xff0c;需要及时介入。
以前面的示例数据为例&#xff0c;如果我们将 Page 作为一个维度&#xff0c;User ID 做 count distinct 度量&#xff0c;构建成 Cube 后得到这样的用户访问统计&#xff08;示例&#xff09;:
△ 表4&#xff1a;样例 Cube 2
这样通过切换 Page 的值来做交集&#xff0c;我们就可以很容易地计算出它们之间的漏斗转化率&#xff0c;如&#xff1a;
select
intersect_count(user_id, page, array[&#39;index.html&#39;]) as first_step_uv,
intersect_count(user_id, page, array[&#39;search.html&#39;]) as second_step_uv,
intersect_count(user_id, page, array[&#39;detail.html&#39;]) as third_step_uv,
intersect_count(user_id, page, array[&#39;index.html&#39;, &#39;search.html&#39;]) as retention_one_two,
intersect_count(user_id, page, array[&#39;search.html&#39;,&#39;detail.html&#39;]) as retention_two_three
from access_log
where dt in (&#39;2016104&#39;, &#39;20161015&#39;, &#39;20161016&#39;)
结果&#xff1a;
5&#xff0c; 3&#xff0c; 2&#xff0c; 3&#xff0c; 2
如此行为漏斗的转化率也就很容易得到了&#xff1a;
△ 表 5&#xff1a;样例行为漏斗结果
当然这是一个很简单的例子&#xff0c;实际会复杂很多&#xff1b;这里的 Page&#xff08;页面&#xff09;可以换成埋点值或其它维度&#xff0c;从而更加细致地分析各种行为之间的关联关系。
前面的例子中都是单个维度值变化时&#xff0c;使用Kylin的交集函数进行留存和漏斗转化的计算&#xff0c;是比较容易理解的。现实中有时候需要在多个维度上同时进行滑动分析&#xff0c;例如运营可能会问&#xff1a;第一天访问“商品明细页”的用户&#xff0c;有多少在第二天访问了“付款页”&#xff1f;那么 Kylin 是否可以做到呢&#xff1f;
答案是肯定的&#xff0c;虽然 intersect_count 交集函数只接受一个维度值的变化&#xff0c;但我们可以巧妙利用 where 做其它维度的筛选&#xff0c;最后的结果交给 SQL 执行器来计算。如&#xff1a;
select
intersect_count(user_id, dt, array[‘20190101’])&#xff0c;#第一天的UV
intersect_count(user_id, dt, array[‘20190101’, ‘20190102’]) #第一天和第二天交集
from access_log
where (dt&#61;&#39;20190101’ and page&#61;‘detail.html’) #筛选第一天&访问明细页的用户or
(dt&#61;‘20190102’ and page&#61;‘payment.html’) #筛选第二天&访问付款页的用户
这样的条件是只需要把 page 和 dt 都做为维度就可以了&#xff0c;是不是很简单&#xff1f;
有时候业务人员在分析问题的时候&#xff0c;会动态调整分析的组合&#xff0c;把一些条件先进行或(or)操作&#xff0c;然后再跟其它条件做与(and)运算。例如&#xff0c;访问了“搜索页”和“详情页”中任何一个的用户&#xff0c;有多少访问了“付款页”&#xff1f;前两者是一个或的关系&#xff0c;它们的结果需要跟第三个进行与操作。
为了支持这种特殊的计算&#xff0c;我们可以扩展 intersect_count 函数&#xff0c;让它可以理解或运算符。下面是一个示例&#xff08;这里默认使用了“|”作为或条件的分隔符&#xff0c;如果维度值中可能包含“|”&#xff0c;需通过配置修改成其它符号&#xff09;&#xff1a;
select
intersect_count(user_id, page, array[&#39;search.html|detail.html’, &#39;payment.html&#39;])
from access_log
在 app 埋点分析中&#xff0c;这也是一个常见的场景。app 开发者为了日后分析的灵活性&#xff0c;会有意埋了很多不同的点&#xff1a;同样的行为在不同客户端、版本中的埋点值可能不同&#xff08;称为“物理埋点”&#xff09;&#xff1b;但业务人员在分析的时候&#xff0c;需要将这些物理埋点根据需要装配成“逻辑埋点”。例如&#xff0c;“安卓端登录”、“苹果端登录”和“网页登录”这三个埋点值都代表了“登录”这个行为&#xff0c;它们或的集合去跟“登录失败”做与操作&#xff0c;可以算出 app 整体登录失败率。
过去为了满足业务的这种灵活分析需求&#xff0c;开发者往往需要调整他们的ETL脚本来改变运算逻辑&#xff0c;周期长、效率低并且很容易出错&#xff1b;使用 Kylin 后就没有这些烦恼&#xff0c;用户可以灵活组装查询条件&#xff0c;剩下的事情交给 Kylin 就可以了。
注&#xff1a;此功能目前还处于内部预览阶段&#xff0c;还未在社区版正式发布。
用户画像分析中需要通过标签进行用户的筛选&#xff1b;作为存储数字集合的最紧凑数据结构&#xff0c;Bitmap 常常被用在用户画像分析中。Kylin 引入 Bitmap 后&#xff0c;也可以用在用户画像的分析。
使用 Kylin 做用户筛查时&#xff0c;通常需要将标签从列转行&#xff0c;将“标签类型”和“标签值”作为维度&#xff0c;将 User ID 作为 Bitmap 度量进行构建&#xff0c;构建后的 Cube 如下&#xff1a;
△ 表 6&#xff1a;样例用户画像 Cube
例如现在要分析&#xff0c;性别是男的、年龄是 90 后的、收入在 10-20 万区间的人有多少&#xff1b;通过 Kylin 这样查询即可&#xff1a;
select
intersect_count(user_id, tag_value, array[&#39;男&#39;, &#39;90后&#39;, &#39;10-20万&#39;])
from user_profile
where (tag_type&#61;&#39;性别&#39; and tag_value&#61;&#39;男&#39;) or (tag_type&#61;&#39;年龄&#39; and tag_value&#61;&#39;90后&#39;) or (tag_type&#61;&#39;收入&#39; and tag_value&#61;&#39;10-20万&#39;)
结果&#xff1a;
2
如果当前标签筛选后的结果集依然很大&#xff0c;用户可以继续添加更多标签&#xff0c;直到将用户数控制到合适的范围&#xff08;例如打算定向发放一万张优惠券&#xff09;。
在用标签筛查后业务人员可能问&#xff0c;能否导出满足这些标签的用户集合明细呢&#xff0c;这样好对他们发券啊&#xff1f;Kylin 是否能在回答用户数的同时&#xff0c;告诉我们具体是哪些 User_ID 呢&#xff1f;
答案是肯定的&#xff0c;因为 Bitmap 忠实地保存了每个 User_ID 值&#xff08;例如使用第 100 个 bit 位代表 ID 为 100 的用户是否出现&#xff09;&#xff0c;因此它可以在需要的时候告诉我们明细数据。
为了支持此类查询&#xff0c;我们可以参照 intersect_count 函数&#xff0c;开发另一个能返回 Bitmap 明细的函数&#xff0c;这里我们暂且称它为 intersect_value 函数&#xff0c;用法跟 intersect_count 一样&#xff0c;只是它的返回类型是一个整数数组&#xff1a;
select
intersect_value(user_id, tag_value, array[&#39;男&#39;, &#39;90后&#39;, &#39;10-20万&#39;])
from user_profile
where (tag_type&#61;&#39;性别&#39; and tag_value&#61;&#39;男&#39;) or (tag_type&#61;&#39;年龄&#39; and tag_value&#61;&#39;90后&#39;) or (tag_type&#61;&#39;收入&#39; and tag_value&#61;&#39;10-20万&#39;)
结果&#xff1a;
[101,103]
有了明细结果&#xff0c;下一步就可以结合其它数据源如用户明细表、CRM 系统等做进一步的分析了&#xff0c;这里就不展开了。
注&#xff1a;此功能目前还处于内部预览阶段&#xff0c;还未在社区版正式发布。
Kylin 的精确去重功能和交集函数最初是由美团点评大数据团队根据自身场景和需求开发并贡献到开源社区的&#xff0c;并在美团内部得到大量使用。现在&#xff0c;这些功能正在被越来越多的用户所使用&#xff0c;有的基于 Kylin 再开发一些前端展现&#xff0c;就实现了能满足业务绝大部分需求的一站式用户行为分析平台。
例如满帮集团&#xff0c;它是原运满满和货车帮合并后的集团&#xff0c;有 8 个手机 app&#xff0c;4 类埋点、上千个埋点值&#xff0c;收集了超过千亿条的用户行为日志。过去他们自己开发的分析平台各方面都不能满足业务需求&#xff0c;束缚了业务的发展&#xff1b;后来满帮集团迁移到了基于 Kylin 的分析平台&#xff0c;借助于 Kylin 的丰富功能&#xff0c;自研了名为 APPDATA 的一站式分析平台&#xff0c;极大地满足了业务对于数据分析的需求。
△ 图2&#xff1a;满帮 APPDATA 数据流程图
这是他们基于 Kylin 的日&#xff0f;周&#xff0f;月留存分析报表&#xff0c;对于留存率异常情况&#xff0c;会通过颜色高亮显示&#xff1a;
△ 图 3&#xff1a;满帮 APPDATA 日留存样例
这是他们开发的、让业务人员可以自定义行为的功能&#xff0c;业务人员可以自由地组合各类埋点&#xff0c;将它们定义称一个“行为”&#xff08;逻辑埋点&#xff09;&#xff1a;
△ 表 4&#xff1a;满帮 APPDATA 自定义用户行为
随后&#xff0c;业务人员可以将一些行为串成一个行为漏斗&#xff0c;然后查看漏斗转化率&#xff0c;背后就是用的 Kylin 交集函数&#xff1a;
△ 表5&#xff1a;满帮 APPDATA 自定义行为漏斗
在这张图上&#xff0c;业务人员可以很方便地查看每一步的留存&#xff0f;流失率&#xff0c;对于异常情况&#xff0c;可以进一步下钻到明细做进一步的筛查。关于满帮使用Kylin的更多信息&#xff0c;请参考文末的“参考”文章【3】。
Kylin 官网文档中有操作指南哦&#xff1a;https://kylin.apache.org/docs/tutorial/create_cube.html
【1】史少锋《如何在 1 秒内做到大数据精准去重》 https://kyligence.io/zh/blog/apache-kylin-count-distinct/
【2】孙业锐 《Retention Or Conversion Rate Analyze in Apache Kylin》https://kylin.apache.org/blog/2016/11/28/intersect-count/
【3】陈雅婕《Kylin 在满帮集团千亿级用户访问行为分析中的应用》https://kyligence.io/zh/resources/kylin_at_manbang_group/
了解更多大数据资讯&#xff0c;点击进入Kyligence官网kyligence.io2019年11月16日Apache Kylin Meetup北京站正在火热报名&#xff01;邀请到滴滴、微众银行、一点资讯以及Kyligence的技术专家为大家呈现精彩应用案例与实践&#xff1a;
【2019年第5期】Apache Kylin Meetup 北京站www.huodongxing.com