用户行为分析系列实践 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 |
英文版