掘金 后端 ( ) • 2024-07-01 15:24

1 业务&实现背景

1.1 业务背景

用户路径分析应用是对用户特定操作的上下游进行可视化展示并分析用户在使用产品时的路径分布情况。

当用户在用我们的产品的时候,是有很多种使用习惯和行为路径的,用户路径可以帮助我们很清晰的看到实际的路径情况,而不是靠主观判断。

通常我们在探究用户使用、来源的时候会思考这些问题

  • 用户在流失/离开预想的路径之后,用户又去做了什么?流向了哪里?
  • 用户在产品界面这么多个功能选项中,究竟选择了哪种?又是怎么使用的?
  • 按转换率从高到低判断,这几条路径哪条转化率最高?核心路径又是什么?
  • “我”做的运营方案/实验、体验优化方案/实验,应该针对哪里优化?实际实施后又是否有效?

这些都是时长在产品运营等实践中的遇到的问题,怎么通过数据产品的用户路径功能,去解决这些问题,满足使用需求,是大数据产品所主要考虑的。

在酷家乐的场景中,主要有以下几个主要用途

  • 探究用户在产品中的主要核心的使用习惯/核心路径
  • 探究用户在不同时间周期的固定路径行为转化情况
  • 探究用户流转流失的现象以及探究其原因

1.2 实现背景

用户的行为操作的数据统一来源于埋点的Hive表, 通过Starrocks查询Hive外表实现数据处理,在通过java代码做最终桑基图数据的提供。

为何选择starrocks不是本文的重点,在此不详细描述,主要用到了starrocks查询外表,物化视图,以及支持bitmap的能力。


2 基本概念

在进行具体的数据模型和架构设计前,先介绍一些基础概念,帮助大家更好的理解本文。

2.1 会话&会话间隔

酷家乐的用户路径是基于会话来做计算的,且支持自定义会话间隔,用户路径的桑基图展示,是为 用户路径 提供一种可视化的分析方式。

会话:即 session,用户自开始访问到结束访问会进行一系列的行为交互,如点击按钮交互、浏览页面交互等,这一系列的行为交互便构成了一个会话。一个人可以产生多次会话,会话的结束依据 会话间隔 而定。

会话间隔: 是两事件被触发时可以接受的最长的时间间隔。

举一个例子让我们快速理解:(以下是三个用户以及其各自的行为序列)

上图中的会话有:

用户 会话 小酷 session1: A-B-A-C  session2: D-B 小家 session3: E-C-A  session4: A 小乐 session5: A

2.2 桑基图

桑基图(Sankey Diagram),也称为桑基能量分流图或桑基能量平衡图,是一种特定类型的流程图,用于可视化系统中不同部分之间的流动关系。图中延伸的分支宽度对应数据流量的大小,直观地显示了各部分之间的流动量。通过桑基图,我们可以清晰地看到各部分之间的流动关系和转化效率,从而更好地理解产品系统的运作,发现潜在的问题,并提出优化方案。

image.png


2.3 Session View

SV即Session View,会话次数,本模型中指出现过该访问路径的会话数。

举例如,
有路径一:A → B → C → D → A → B ,
和路径二:A → B → C ,
路径一和路径二为两个不同的session

那么,A → B 的 SV 为 1+1=2。


3 数据模型

3.1 总体流程思路

3.2 详细流程

由上图所示,其实主要的数据处理流程分为两步:

  1. 计算中间汇总数据
  2. 封装桑基图结构

所以我们就详细介绍这两步


3.2.1 starrocks计算中间数据汇总

3.2.1.1 对事件源数据做筛序处理

在这一步,会按照用户选择的事件做数据源的获取,分别得到对应埋点事件(可筛选)的数据; 且可以对用户来源做筛选,任意匹配规则,支持对用户属性、用户分群等其他基本信息的嵌套规则筛选。

对于分群和属性的筛选,分群表和标签表我们设计成bitmap表,用bitmap表直接做判断包含userid的筛选,比join会快很多。



3.2.1.2 session划分事件流

对于session会话,会话间隔相关的内容在 2.1 小节已经介绍

这一步做的操作:

根据用户对于session的时间配置和每一个用户事件之间的间隔(当前记录的时间 - 上一条记录的时间)来划分session,生成sessionId


伪代码

count (if(date_diff('minute', time, next_time) > 30, 1, null)) over (partition by userId order by time, action) sessionId`

假设有用户 a 和用户 b,a 用户当天发生的行为事件分别为 E1, E2, E3... ,

事件发生的时间分别为 T1, T2, T3... ,选定的session间隔为sessionPeriod。

如图所示  T4-T3 > sessionPeriod,所以 E1, E2, E3 被划分到了session0,E4, E5 被划分到了session1;

同理  T6-T5 > sessionPeriod,所以E6及后面用户行为操作被划分到了session2。


3.2.1.3 相邻事件去重

这一步做的操作:

  • 开窗lag()按照每一个用户取上一条事件记录的事件名
  • 将前后一致的事件去重

伪代码

lag (action, 1, '-1') over(partition by userId order by time, action) next_action 

where action <> next_action


3.2.1.4 按起始/终止事件来割取路径

这一步做的操作:

  • 开窗根据userid、sessionId分组,取初始/终止事件为{目标事件}的事件做标记,赋予actionId;
  • 取actionId大于等于1的数据,这部分则为我们继续计算的数据

伪代码

count (if(action = '起始事件', 1, null)) over (partition by userId, sessionId order by time desc, action) actionId 

where  actionId >= 1

得到的结果样例数据如下图所示:

比如userid为1003的路径明细数据,其不同session的通过起始事件A切割后的处理路径如下

image.png



3.2.1.5 聚合每一条路径的会话数

通过 event1~10 分组,聚合count计算出每一条路径的会话数

样例数据如下图所示:

image.png



3.2.1.6 得到每一条路径的深度

最后用eventn是否为null,来计算出每一条路径的深度

伪数据如下图所示:

image.png



3.2.2 封装桑基图结构

3.2.2.1 获取路径数据

若是现查,那么按照上面的步骤已经得到了每一条路径的聚合路径数;

若是后续查,则会直接查询物化视图中的数据,物化视图中的数据是3.2.1.4的结果,聚合成3.2.1.6的结果。



为什么不直接缓存结果数据,而选择用物化视图物化中间数据

首先,对于桑基图本身来说,直接缓存结果数据没有问题;但是对于桑基图中的元素的后续联动交互来说,后续可以选择图中元素做人群的圈选,那么就得保留到每一个用户粒度的明细数据(或者可以再封装聚合成桑基图上节点、边的数据,并保留下来也行,本方案没有采用),其实根本原因是因为至此我们拿到的数据还不是桑基图的聚合数据,只是每一条路径的聚合数据。且这边使用异步物化视图还可以定时做更新,也可以手动刷新,物化的成本也比较小,所以最终采用了这种方案。

下图样例展示了通过桑基图元素继续探究,查看这部分转化人群:

3.2.2.2 带权树

带权树主要由树的节点和树枝组成,如下图所示

其实对应的就是桑基图的Node节点和Edge边,故下文就以Node和Edge来叙述。

我们不难发现,带权树几乎和桑基图的结构几乎一致了,但是上例的level2中我们发现出现了多个节点是重复的,而桑基图中其实是汇集的,所以我们需要将每一个level的node管理起来,对于每一个事件,只映射一个node就能达到我们想要的结果。

所以就用了下面描述的层级Map去实现了。



3.2.2.3 层级Map

我们将树横过来,更贴近于桑基图本身,并且同一层都由一个Map统一的做管理,避免node重复,如下:

这样就完全符合了桑基图的结构。


那么我们该怎么将每一条数据的统计值给封装入这样一个结构中呢

其实只要去遍历每一条路径,分别将sv加到每一个路过的元素上就行了。



3.2.2.4 总体封装桑基图结构

总体桑基图数据结构基本如下图所示:

image.png



3.2.2.5 转化率、终止事件

转化率等由前端将前后会话数相除处理得到。

上述都是正向的起始事件固定的统计计算,终止事件情况的实现和起始事件几乎完全一致,只是排序等都取的是倒序,还有对于起始来说的“流失“在终止情况中是没有的,转而有了”从其他路径汇入“的概念。