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