用户行为分析系列实践 9 – 枚举和标签维度
目标任务
用户事件表T结构和部分数据示例如下:
Time | UserID | EventType | OS | Browser | … | f1 | f2 | f3 | f4 | f5 | … |
2022/6/1 10:20 | 1072755 | Search | Android | IE | … | true | false | false | true | false | … |
2022/6/1 12:12 | 1078030 | Browse | IOS | Safari | … | false | false | true | true | true | … |
2022/6/1 12:36 | 1005093 | Submit | Android | Chrome | … | true | true | true | false | false | … |
2022/6/1 13:21 | 1048655 | Login | Windows | Chrome | … | false | false | true | true | true | … |
2022/6/1 14:46 | 1037824 | Logout | Android | Edge | … | false | false | false | true | true | … |
2022/6/1 15:19 | 1049626 | AddtoCart | Windows | Edge | … | true | true | false | true | false | … |
2022/6/1 16:00 | 1009296 | Submit | IOS | Firefox | … | false | true | false | false | true | … |
2022/6/1 16:39 | 1070713 | Browse | IOS | Sogou | … | true | true | true | false | false | … |
2022/6/1 17:40 | 1090884 | Search | Windows | IE | … | true | false | true | true | false | … |
T表字段说明:
字段名 | 数据类型 | 字段含义 |
Time | 日期时间 | 事件发生的时间戳,精确到毫秒 |
UserID | 字符串 | 用户ID |
EventType | 字符串 | 事件类型 |
OS | 字符串 | 操作系统,取值为Android,IOS,Windows,……,Unknown |
Browser | 字符串 | 浏览器,取值为IE,Safari,Edge,Firefox,Chrome,Sogou,……,Unknown |
… | 字符串 | 更多其它取值为枚举值的字段 |
f1 | 布尔值 | 是否异地发生,取值为真和假 |
f2 | 布尔值 | 是否惯用设备,取值为真和假 |
f3 | 布尔值 | 是否惯用浏览器,取值为真和假 |
f4 | 布尔值 | 是否是手机,取值为真和假 |
f5 | 布尔值 | 是否首次操作,取值为真和假 |
… | 布尔值 | 更多其它取值为真和假的字段 |
计算任务:
统计指定时间段内,本地使用安卓/苹果手机,使用Safari/ Edge/ Chrome,且非首次操作的用户,在每种事件类型下的发生次数,以及去重用户数。
实践技能
关于枚举和标签维度的相关知识可参考:
1. 将枚举维度值转换成序号
根据OS和Browser字段的取值列表,把事件表T中相应字段值转换成在此列表中的序号
2. 将二值维度值转成为位
将事件表T中的f1,…这些取值为true/false的二值字段,每16个拼成一组,用一个整数字段的位来存储,一个整数字段存储16个原字段。说明:SPL对16位整数做了优化,读取速度更快,所以一般用16位整数来存储,如果二值维度超出16个,则用多个整数字段。
经过1,2动作转换后的T表结构如下:
字段名 | 数据类型 | 字段含义 |
Time | 日期时间 | 事件发生的时间戳,精确到毫秒 |
UserID | 字符串 | 用户ID |
EventType | 字符串 | 事件类型 |
OS | 整数 | 操作系统,取值为枚举序列中的序号 |
Browser | 整数 | 浏览器,取值为枚举序列中的序号 |
… | … | … |
b1 | 整数 | 整数字段,用位存储二值字段 |
b2 | 整数 | 整数字段,用位存储二值字段 |
… | … | … |
3. 使用转换后的T表进行数据统计
采用SPL提供的序号引用和位式运算实现统计。
示例代码
1、 转储T表的基本方法
A | |
10 | =connect("demo").cursor@x("select * from T order by Time") |
11 | =file("OS.txt").import@i() |
12 | =file("Browser.txt").import@i() |
13 | =A10.new(Time,UserID,EventType,A11.pos(OS):OS,A12.pos(Browser):Browser,……,bits@n(f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16):b1,bits@n(f17,…..,f32):b2,……) |
14 | =file("T.ctx").create(#Time,UserID,EventType,OS,Browser,……,b1,b2,……) |
15 | =A14.append(A13) |
16 | >A14.close() |
A10 读事实表,按Time排序
A11 读入OS字段可能取值的列表
A12 读入Browser字段可能取值的列表
A13 将OS字段值转换成其在A11中的序号,将Browser字段值转换成其在A12中的序号,如果取列表是有序的,可以在pos时加@b选项用二分法定位,速度更快。
依次将取值为true/false的二值字段每16个一组按位存储成一个整数字段,1代表true,0代表false。bits@n表示将true/false变成1/0
A14 产生组表文件的结构
A15 把A13输出到A14组表文件中
2、 任意多个枚举维度
如果枚举字段数较多,可以让列表值文件名和字段名相同,自动生成相应的表达式,如:
A | |
6 | [OS,Browser] |
7 | =A6.(file(~/".txt").import@i()) |
8 | =A6.("A7("/#/").pos("/~/"):"/~).concat@c() |
A6 枚举字段名序列
A7 读取枚举字段取值列表,返回结果是:
A8 生成将枚举字段值转换成其在枚举序列中的序号的表达式,其返回结果是:
A7(1).pos(OS):OS,A7(2).pos(Browser):Browser
把A8生成的表达式,用宏替换符号${A8}的形式插入到前面的A13表达式,即:
=A10.new(Time,UserID,EventType, ${A8}, bits@n(f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16):b1, bits@n(f17,…..,f32):b2,……)
3、 任意多个二值维度
如果二值字段数很多,且字段命名很有规律,假设都是fn,可以自动生成bits表达式,如:
A | |
1 | >n=50 |
2 | =n\16 |
3 | =A2.((a=(#-1)*16,"bits@n("+16.("f"/(~+a)).concat@c()+"):b"/#)) |
4 | ="bits@n("+to(A2*16+1,n).("f"/~).concat@c()+"):b"/(A2+1) |
5 | =(A3|A4).concat@c() |
A1 二值字段数
A2 计算需要几个整数来存储
A3 自动拼接生成bits@n表达式
A4 把末尾不足16个的字段作为最后一个整数字段生成对应表达式
A5的返回结果是:
bits@n(f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16):b1,bits@n(f17,f18,f19,f20,f21,f22,f23,f24,f25,f26,f27,f28,f29,f30,f31,f32):b2,bits@n(f33,f34,f35,f36,f37,f38,f39,f40,f41,f42,f43,f44,f45,f46,f47,f48):b3,bits@n(f49,f50):b4
把A5生成的表达式,用宏替换符号${A5}的形式插入到前面的A13表达式,即:
=A10.new(Time,UserID,EventType, ${A8}, ${A5})
4、 完整的转储代码
A | |
1 | >n=50 |
2 | =n\16 |
3 | =A2.((a=(#-1)*16,"bits@n("+16.("f"/(~+a)).concat@c()+"):b"/#)) |
4 | ="bits@n("+to(A2*16+1,n).("f"/~).concat@c()+"):b"/(A2+1) |
5 | =(A3|A4).concat@c() |
6 | [OS,Browser] |
7 | =A6.(file(~/".txt").import@i()) |
8 | =A6.("A7("/#/").pos("/~/"):"/~).concat@c() |
9 | =connect("demo").cursor@x("select * from T order by Time") |
10 | =A9.new(Time,UserID,EventType, ${A8}, ${A5}) |
11 | =file("T.ctx").create(#Time,UserID,EventType,OS,Browser,……,b1,b2,……) |
12 | =A11.append(A10) |
13 | >A11.close() |
5、 对转换后的事实表统计的基本方法
“本地使用安卓/苹果手机,使用Safari/ Edge/ Chrome,且非首次操作的用户”这个过滤条件对应的是f1为0,f4为1,f5为0,OS取值为Android/IOS,Browser取值为Safari/ Edge/ Chrome
A | |
12 | =bits(0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0) |
13 | =bits(1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0) |
14 | =file("OS.txt").import@i() |
15 | =A14.(["Android","IOS"].contain@b(~)) |
16 | =file("Browser.txt").import@i() |
17 | =A16.(["Chrome","Edge","Safari"].contain@b(~)) |
18 | >start=date("2022-03-15","yyyy-MM-dd"),end=date("2022-06-16","yyyy-MM-dd") |
19 | =file("T.ctx").open().cursor(UserID, EventType;Time>=start && Time<=end && A15(OS) && A17(Browser) && and(b1,A13)==A12).groups(EventType; count(1):Num, icount(UserID):iNum) |
A12 根据过滤条件,将条件为真的第四位置为1其它位置为0
A13 根据过滤条件,将有条件的第一位、第四位、第五位置为1其它位置为0
A14 读入OS字段的可能取值序列
A15 根据过滤条件,生成一个与A14同长度的序列,其成员值为对应成员是否满足过滤条件。
A16 读入Browser字段的可能取值序列
A17 根据过滤条件,生成一个与A16同长度的序列,其成员值为对应成员是否满足过滤条件。
A19 在游标中对T表进行过滤,然后再分组汇总。这里用到的位运算,and(b1,A13)==A12表示满足第四位为1、第一位和第五位为0的过滤条件。
运行结果:
EventType | Num | iNum |
AddtoCart | 945674 | 85476 |
Browse | 1778901 | 178791 |
Login | 2033000 | 193400 |
Logout | 2033000 | 193400 |
Search | 1547931 | 127539 |
Submit | 467345 | 43375 |
6、 任意多个枚举维度过滤的统计
当需要过滤的枚举维度比较多时,同样的可以自动生成表达式,如:
A | |
12 | ="{OS:[Android,IOS],Browser:[Chrome,Edge,Safari]}" |
13 | =json(A12) |
14 | =A13.fname() |
15 | =A14.(file(~/".txt").import@i().(A13.field(A14.~).contain(~))) |
16 | =A14.("A15("/#/")("/~/")") |
17 | =A16.concat("&&") |
A12 需要过滤的枚举字段名和过滤值组成的json串
A13 把A12中的json串转成序表
A14 获得A13中的字段名序列
A15 生成枚举字段的对位布尔值序列,如果A12中传入的枚举值序列是有序的,contain可以加@b选项速度更快
A16 生成过滤表达式序列
A17 生成过滤表达式串:A15(1)(OS) && A15(2)(Browser)
把A17生成的表达式,用宏替换符号${A17}的形式插入到前面的A19表达式,即:
=file("T.ctx").open().cursor(UserID, EventType;Time>=start && Time<=end && ${A17} && and(b1,A13)==A12).groups(EventType; count(1):Num, icount(UserID):iNum)
7、 任意多个二值维度过滤的统计
如果需要过滤的二值维度比较多,可以自动生成表达式,如:
A | |
1 | [1,5,22] |
2 | [4,20] |
3 | 50 |
4 | =[0]*A3 |
5 | >A4(A1)=1 |
6 | =[0]*A3 |
7 | >A6(A1|A2)=1 |
8 | =A4.group((#-1)\16) |
9 | =A6.group((#-1)\16) |
10 | =A8.len().("and(A9("/~/"),b"/~/")==A8("/~/")") |
11 | =A10.concat("&&") |
A1 过滤条件中要求为真的二值字段序号
A2 过滤条件中要求为假的二值字段序号
A3 二值字段的总个数
A4 产生一个值为0长度为A3的序列
A5 把A4中对应需要为真的成员赋值1
A6 产生一个值为0长度为A3的序列
A7 把A6中对应需要过滤的成员赋值1
A8 把A4拆成16个一组
A9 把A6拆成16个一组
A10 生成过滤表达式序列
A11 生成过滤表达式串
and(A9(1),b1)==A8(1) && and(A9(2),b2)==A8(2) && and(A9(3),b3)==A8(3) && and(A9(4),b4)==A8(4)
把A11生成的表达式,用宏替换符号${A11}的形式插入到前面的A19表达式,即:
=file("T.ctx").open().cursor(UserID, EventType;Time>=start && Time<=end && ${A17} && ${A11}).groups(EventType; count(1):Num, icount(UserID):iNum)
8、 完整的代码
A | |
1 | [1,5,22] |
2 | [4,20] |
3 | 50 |
4 | =[0]*A3 |
5 | >A4(A1)=1 |
6 | =[0]*A3 |
7 | >A6(A1|A2)=1 |
8 | =A4.group((#-1)\16) |
9 | =A6.group((#-1)\16) |
10 | =A8.len().("and(A9("/~/"),b"/~/")==A8("/~/")") |
11 | =A10.concat("&&") |
12 | ="{OS:[Android,IOS],Browser:[Chrome,Edge,Safari]}" |
13 | =json(A12) |
14 | =A13.fname() |
15 | =A14.(file(~/".txt").import@i().(A13.field(A14.~).contain(~))) |
16 | =A14.("A15("/#/")("/~/")") |
17 | =A16.concat("&&") |
18 | >start=date("2022-03-15","yyyy-MM-dd"),end=date("2022-06-16","yyyy-MM-dd") |
19 | =file("T.ctx").open().cursor(UserID, EventType;Time>=start && Time<=end && ${A17} && ${A11}).groups(EventType; count(1):Num, icount(UserID):iNum) |
英文版