用户行为分析系列实践 11 – 有序分组
目标任务
用户事件表T结构和部分数据示例如下:
Time |
UserID |
EventType |
… |
2022/6/1 10:20 |
1072755 |
Search |
… |
2022/6/1 12:12 |
1078030 |
Browse |
… |
2022/6/1 12:36 |
1005093 |
Submit |
… |
2022/6/1 13:21 |
1048655 |
Login |
… |
2022/6/1 14:46 |
1037824 |
Logout |
… |
2022/6/1 15:19 |
1049626 |
AddtoCart |
… |
2022/6/1 16:00 |
1009296 |
Submit |
… |
2022/6/1 16:39 |
1070713 |
Browse |
… |
2022/6/1 17:40 |
1090884 |
Search |
… |
… |
… |
… |
… |
T表字段说明:
字段名 |
数据类型 |
字段含义 |
Time |
日期时间 |
事件发生的时间戳,精确到毫秒 |
UserID |
字符串 |
用户ID |
EventType |
字符串 |
事件类型 |
… |
… |
… |
计算任务:
统计指定时间段内,一个时间窗口期内依次发生过搜索、加购物车、提交订单这三个动作中前N个的用户数量,从而方便后续统计用户转化率和用户流失率,该问题也称为漏斗转化分析。
需要注意的事项:
1、上述三个事件必须按时间顺序依次发生,顺序不对的不算
2、上述三个事件必须是同一个用户在一个时间窗口期内发生,超出时间窗口期则不算
3、以第一个事件发生时间点开始计时,如果后续事件在时间窗口期内按顺序发生,则相应事件记为1次,否则为0次,一旦出现某个事件为0次,后续事件就不用继续扫描。
实践技能
关于漏斗转化分析的相关知识可参考:
从已经按帐户、时间有序的数据中依次读出每个帐户的数据进入内存后做复杂运算。
本例的算法比较复杂,在外存中很不方便,需要全内存计算。而一个用户的数据量很小,可以全部读进内存。
对于已经按用户和时间有序的数据,在使用游标读取时,可以每次只读入一个用户的全部数据,且这些数据按发生时间排序的,然后在内存中实施漏斗统计。
示例代码
根据前面章节的介绍,把T表的数据按照UserID,Time排序后,转储到T.ctx。
然后使用游标循环,每次只取一个用户的数据,进行漏斗转化计算
A |
B |
C |
D |
|
1 |
>start=date("2022-03-15","yyyy-MM-dd"),end=date("2022-04-05","yyyy-MM-dd"),tw=7 |
|||
2 |
[Search,AddtoCart,Submit] |
=A2.(0) |
||
3 |
=file("T.ctx").open().cursor(UserID, EventType,Time;Time>=start && Time<=end && A2.contain(EventType)) |
|||
4 |
for A3;UserID |
=first=A4.select@1(EventType==A2(1)) |
||
5 |
if(B4==null) |
next |
||
6 |
=t=null |
=A2.(null) |
||
7 |
for A2 |
if #B7==1 |
>C6(1)=t=t1=first.Time |
|
8 |
else |
>C6(#B7)=t=if(t,A4.select@1(EventType==B7 && Time>t && Time<elapse(t1,tw)).Time,null) |
||
9 |
=C6.(if(~,1,0)) |
|||
10 |
>D2=D2++B9 |
|||
11 |
return D2 |
A1 定义时间段参数、时间窗口tw,实际使用中通过参数传入
A2 目标事件名称,其顺序是重点
D2 产生一个和A2等长的数组,用于存储每个事件的发生次数,此为最终返回结果
A3 打开组表文件,产生游标,游标中对时间段和目标事件进行过滤
A4 对游标循环,每次取出一个用户的所有数据
B4 读第一个事件第一次发生的记录,赋给first变量
B5 如果第一个事件没有发生,则跳转下一个用户,当前用户为无效用户,不需要继续统计
B6 定义变量t,用于存储后续循环中的当前事件发生的时间
C6 定义和A2等长的数组,用于存储后续循环中每一个事件对应的发生时间
B7 循环A2
C7-D7 如果B7的循环序号是1,表明当前为第一个事件,此时记录t为first的发生时间,同时把此时间赋给t1
C8-D8 如果不是第一个事件,则判断上一个t是否为空,如果为空则把当前t赋值null;如果不为空,则从满足>t且小于时间窗口t1+tw的数据中寻找最早发生的当前事件记录,把其发生时间赋给t
B9 循环C6,把时间为null的次数记为0,不为null的次数记为1
B10 把B9累加到D2
上述计算还可以改成并行计算的方式,进一步提高性能:
A |
B |
C |
D |
E |
|
1 |
>start=date("2022-03-15","yyyy-MM-dd"),end=date("2022-04-05","yyyy-MM-dd"),tw=7 |
||||
2 |
[Search,AddtoCart,Submit] |
||||
3 |
=file("T.ctx").open().cursor@m(UserID, EventType,Time;Time>=start && Time<=end && A2.contain(EventType);2) |
||||
4 |
fork A3 |
=A2.(0) |
|||
5 |
for A4;UserID |
=first=B5.select@1(EventType==A2(1)) |
|||
6 |
if(first==null) |
next |
|||
7 |
=t=null |
=A2.(null) |
|||
8 |
for A2 |
if #C8==1 |
>D7(1)=t=t1=first.Time |
||
9 |
else |
>D7(#C8)=t=if(t,B5.select@1(EventType==C8 && Time>t && Time<elapse(t1,tw)).Time,null) |
|||
10 |
=D7.(if(~,1,0)) |
||||
11 |
>B4=B4++C10 |
||||
12 |
return B4 |
||||
13 |
return transpose(A4).(~.sum()) |
A3 cursor加上@m选项,产生多路游标
A4 fork A3产生多个线程,并行计算
B4 产生和A2等长的数组,用于存放当前线程的计算结果
B12 当前线程的计算结果返回,存至A4中,组成一个长度等于线程数的序列
A13 把A4中的结果转置后求和,即得到最终结果
运行结果:
Member |
393400 |
257539 |
83375 |
英文版