有序集合上的定位计算
【摘要】
在数据计算时,除了每条记录本身,经常也会关心有序集合中与位置相关的计算。例如:编号为 752084 的订单在销售表中的第几行?2019 年上证指数收盘价最高的是第几个交易日?如何简单快捷的实现定位?这里将为你全程剖析,并提供 esProc SPL 示例代码。有序集合上的定位计算
1. 定位成员
定位成员是指在有序集合中通过比较成员来查找位置。
【例 1】根据每周的课程表,查看每节课适合的任课教师有哪些。下面任课教师表中,第一列是教师姓名,第三列起是课程代码(null 表示空)。任课教师表部分数据如下:
Teachers.txt |
||||||||
Petitti |
Matematica |
mif |
mig |
vif |
vig |
null |
null |
… |
Canales |
Apesca |
luc |
lud |
mac |
mad |
mic |
mid |
… |
Lucero |
NavegacionI |
lub |
luc |
lud |
lue |
mab |
mac |
… |
Bergamaschi |
TecPesc |
lua |
luf |
maa |
maf |
mia |
mif |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
课程表数据如下:
Monday |
Tuesday |
Wednesday |
Thursday |
Friday |
lua |
maa |
mia |
jua |
via |
lub |
mab |
mib |
jub |
vib |
luc |
mac |
mic |
juc |
vic |
lud |
mad |
mid |
jud |
vid |
lue |
mae |
mie |
jue |
vie |
luf |
maf |
mif |
juf |
vif |
lug |
mag |
mig |
jug |
vig |
【SPL脚本】
A |
B |
|
1 |
=file("Teachers.txt").import() |
/导入任课老师表 |
2 |
=A1.new(#1:professor,~.array().to(3,A1.fno()).select(~):codeArray) |
/由任课教师表生成新表,其中第一列是教师名,第二列是课程列表。 |
3 |
=file("Courses.txt").import@t().conj(~.array()) |
/导入课程表,然后将所有课程合并在一起 |
4 |
=A3.(A2.select(codeArray.pos(A3.~)).(professor)) |
/循环课程,使用 pos 函数在教师的课程列表中查找当前课程,选出每节课适合的教师。 |
5 |
=create(Monday,Tuesday,Wednesday,Thursday,Friday).record(A4.(~.concat@c())) |
/创建周一到周五的课程表,并依此填入教师。 |
A5的执行结果如下:
Monday |
Tuesday |
Wednesday |
Thursday |
Friday |
Bergamaschi,Puebla |
Bergamaschi,Pue… |
Bergamaschi,Puebla |
Bergamaschi,Pue… |
Bergamaschi,Puebla |
Lucero,Puebla,Lu… |
Lucero,Mazza,Pu… |
Lucero,Puebla,Chi… |
Lucero,Mazza,Pe… |
Lucero,Puebla,Vel… |
Canales,Lucero,P… |
Canales,Lucero,M… |
Canales,Lucero,P… |
Canales,Lucero,M… |
Lucero,Velasco,Lu… |
… |
… |
… |
… |
… |
在集合中定位成员时,成员可能重复出现。例如定位数学 100 分的同学可能有多个,名字叫 Ashley 的员工可能不止一人等等。
【例 2】 根据销售表和客户表,查询 2014 年无销售记录的客户。销售表和客户表的关系如下:
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from Sales where year(OrderDate)=2014") |
/查询 2014 年的销售记录 |
3 |
=A1.query("select * from Customer") |
/查询客户表 |
4 |
=A3.(ID).sort() |
/列出客户序号并排序 |
5 |
=A2.align(A4.len(), A4.pos@b(CustomerID)) |
/销售数据按照客户序号进行定位分组,使用 pos 函数查找客户序号。由于客户序号是有序的,这里使用 @b 选项进行二分法查找,可以更快速定位。 |
6 |
=A3(A5.pos@a(null)) |
/使用 pos() 函数的 @a 选项,选出所有没有销售记录(值为 null)的客户信息,否则只会选出一个。 |
A6的执行结果如下:
ID |
Name |
State |
… |
ALFKI |
CMA-CGM |
Texas |
… |
CENTC |
Nedlloyd |
Florida |
… |
2. 定位最大值 / 最小值
定位最大值 / 最小值,是指获取最大值 / 最小值成员(或表达式)所在的序号。
【例 3】 根据股市交易表,统计上证指数收盘价最高的当天,相对前日的涨幅。部分数据如下:
Date |
Open |
Close |
Amount |
2019/12/31 |
3036.3858 |
3050.124 |
2.27E11 |
2019/12/30 |
2998.1689 |
3040.0239 |
2.67E11 |
2019/12/27 |
3006.8517 |
3005.0355 |
2.58E11 |
2019/12/26 |
2981.2485 |
3007.3546 |
1.96E11 |
2019/12/25 |
2980.4276 |
2981.8805 |
1.9E11 |
… |
… |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=file("000001.csv").import@ct() |
/导入数据文件 |
2 |
=A1.sort(Date) |
/按日期排序 |
3 |
=A2.pmax(Close) |
/使用函数 A.pmax(),获取收盘价最大值的成员序号。 |
4 |
=A2.calc(A3,Close/Close[-1]-1) |
/使用当天收盘价和前日收盘价计算涨幅 |
A4的执行结果如下:
Member |
0.010276967857506536 |
同样可以使用函数 A.pmin() 来取最小值的成员序号:
A |
B |
|
3 |
=A3.pmin(Close) |
/使用函数 A.pmin(),获取收盘价最小值的成员序号。 |
最大值所在记录不一定是唯一的,如果想返回所有记录的序号,可以使用函数A.pmax()的选项@a :
A |
B |
|
3 |
=A2.pmax@a(Close) |
/使用函数 A.pmin() 的 @a 选项,取出所有股市最高点记录的序号 |
如果希望从后向前定位,可以使用函数A.pmax()的选项@z :
A |
B |
|
3 |
=A2.pmax@z(Close) |
/使用函数 A.pmax() 的 @z 选项,从后向前取出股市最高点记录的序号 |
3. 根据条件定位
根据条件定位,是指对有序集合的成员按指定条件进行计算,返回结果为 "true" 的成员序号。例如在员工表中查找年龄大于 50 岁的员工序号,在成绩表中查找数学分数高于 90 分的成绩序号等等。
【例 4】 根据股市交易表,统计收盘价涨幅超过 3% 的交易日,相对前日的交易量涨幅。部分数据如下:
Date |
Open |
Close |
Amount |
2019/12/31 |
3036.3858 |
3050.124 |
2.27E11 |
2019/12/30 |
2998.1689 |
3040.0239 |
2.67E11 |
2019/12/27 |
3006.8517 |
3005.0355 |
2.58E11 |
2019/12/26 |
2981.2485 |
3007.3546 |
1.96E11 |
2019/12/25 |
2980.4276 |
2981.8805 |
1.9E11 |
… |
… |
… |
… |
【SPL脚本】
A |
B |
|
1 |
=file("000001.csv").import@ct() |
/导入数据文件 |
2 |
=A1.select(year(Date)==2019).sort(Date) |
/选出 2019 年的股市记录并按日期排序 |
3 |
=A2.pselect@a(Close/Close[-1]>1.03) |
/使用函数 A.pselect () 取出股市收盘价涨幅超过 3% 的记录序号,@a 选项会返回所有满足条件的成员序号。 |
4 |
=A3.new(A2(~).Date:Date, A2(~).Amount/A2(~-1).Amount:'Amount increase') |
/循环用每天交易量和前日交易量计算涨幅 |
A3的执行结果如下:
Member |
161 |
187 |
211 |
A4的执行结果如下:
Date |
Amount increase |
2019/02/25 |
1.758490566037736 |
2019/03/29 |
1.3344827586206895 |
2019/05/10 |
1.3908629441624365 |
我们可以看到,收盘价涨幅超过 3% 的这三天,交易量较前日大幅提升。
4. 区间定位
前面介绍了根据成员或者表达式来定位,有时候我们还需要通过区间定位,从而进行分组汇总等计算。例如年龄集合 [0,18,35,60] 分别代表少年、青年、中年和老年,查找年龄 20 在集合中的区段序号,结果是 2,也就是青年;工资集合定义为[0,8000,15000,30000],查找工资 25000 在集合中的区段序号,结果是 3。
【例 5】 根据员工薪资表,统计 8000 以下、8000~12000 和 12000 以上每个薪资分段的员工总数。部分数据如下:
ID |
NAME |
BIRTHDAY |
SALARY |
1 |
Rebecca |
1974-11-20 |
7000 |
2 |
Ashley |
1980-07-19 |
11000 |
3 |
Rachel |
1970-12-17 |
9000 |
4 |
Emily |
1985-03-07 |
7000 |
5 |
Ashley |
1975-05-13 |
16000 |
… |
… |
… |
… |
【SPL脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from EMPLOYEE") |
/查询员工表 |
3 |
[0,8000,12000] |
/定义工资区间 |
4 |
=A2.align@a(A3.len(),A3.pseg(SALARY)) |
/使用 A.pseg(y) 函数获取工资所在区间序号 |
5 |
=A4.new(A3 (#):SALARY,~.count():COUNT) |
/统计每组的人数 |
A5的执行结果如下:
SALARY |
COUNT |
0 |
308 |
8000 |
153 |
12000 |
39 |
有时候集合的区段值需要经过计算后再计算成员在集合中的区段序号。
【例 6】 根据员工薪资表,统计入职 10 年以下、10~20 年和 20 年以上每组的员工平均工资。部分数据如下:
ID |
NAME |
HIREDATE |
SALARY |
1 |
Rebecca |
2005-03-11 |
7000 |
2 |
Ashley |
2008-03-16 |
11000 |
3 |
Rachel |
2010-12-01 |
9000 |
4 |
Emily |
2006-08-15 |
7000 |
5 |
Ashley |
2004-07-30 |
16000 |
… |
… |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from EMPLOYEE") |
/查询员工表 |
3 |
[0,10,20] |
/定义入职年限区间 |
4 |
=A2.align@a(A3.len(),A3.pseg(year(now())-~,year(HIREDATE))) |
/使用函数 A.pseg(x,y) 获取入职时间所在区间 |
5 |
=A4.new(A3(#):EntryYears,~.avg(SALARY):AvgSalary) |
/统计每组的平均工资 |
A5的执行结果如下:
EntryYears |
AvgSalary |
0 |
6807.69 |
10 |
7417.78 |
20 |
7324.32 |
5. 获取排序后在原集合的位置
将有序集合的成员,通过一定的方法按关键字顺序排列的过程叫做排序。其目的是将一组“无序”的记录集合调整为“有序”的记录集合。有时候有序集合的原序是有意义的,例如查询出来的订单表是按时间顺序生成的,现在要按照销售额进行排序。但是时间顺序在后续的计算中还需要使用,希望按新条件排序的同时可以保留原序。
【例 7】 根据员工表,求年龄最大的三名员工的入职顺序。部分数据如下:
ID |
NAME |
BIRTHDAY |
HIREDATE |
1 |
Rebecca |
1974-11-20 |
2005-03-11 |
2 |
Ashley |
1980-07-19 |
2008-03-16 |
3 |
Rachel |
1970-12-17 |
2010-12-01 |
4 |
Emily |
1985-03-07 |
2006-08-15 |
5 |
Ashley |
1975-05-13 |
2004-07-30 |
… |
… |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from EMPLOYEE order by HIREDATE") |
/使用函数 A.psort() 查询员工表并按入职时间排序 |
3 |
=A2.psort(BIRTHDAY) |
/获取员工生日排序后在排序前的序号 |
4 |
=A2(A3.to(3).sort()) |
/在员工表中按生日前三的员工序号选出 |
A4的执行结果如下:
ID |
NAME |
BIRTHDAY |
HIREDATE |
296 |
Olivia |
1968-11-05 |
2006-11-01 |
440 |
Nicholas |
1968-11-24 |
2008-07-01 |
444 |
Alexis |
1968-11-12 |
2010-12-01 |
6. 集合的整体定位
前面介绍了单个成员的定位运算,下面要介绍集合的整体定位。例如在集合 [a,b,c,d,e] 中定位集合 [c,d,a],返回结果为[3,4,1]。再比如在集合[a,b,c,d,e] 中定位集合[c,f],虽然 c 存在,但是 f 不存在,所以返回结果为空。
【例 8】 根据发帖记录表,按标签分组并统计各个标签出现频数。部分数据如下:
ID |
TITLE |
Author |
Label |
1 |
Easy analysis of Excel |
2 |
Excel,ETL,Import,Export |
2 |
Early commute: Easy to pivot excel |
3 |
Excel,Pivot,Python |
3 |
Initial experience of SPL |
1 |
Basics,Introduction |
4 |
Talking about set and reference |
4 |
Set,Reference,Dispersed,SQL |
5 |
Early commute: Better weapon than Python |
4 |
Python,Contrast,Install |
… |
… |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from PostRecord") |
/查询发帖记录表 |
3 |
=A2.conj(Label.split(",")).id() |
/将标签按逗号分隔后合并到一起,获得没有重复值的全部标签。 |
4 |
=A2.align@ar(A3.len(),A3.pos(Label.split(","))) |
/使用函数 A.pos()整体定位帖子的标签在全部标签中的位置。再使用 align() 函数的 @r 选项,按照定位分组。 |
5 |
=A4.new(A3(#):Label,~.count():Count).sort@z(Count) |
/统计每个标签的帖子数量,按降序排列 |
A5的执行结果如下:
Label |
Count |
SPL |
7 |
SQL |
6 |
Basics |
5 |
… |
… |
7. 判断是否集合成员
有时候我们并不关心集合 B 的成员在集合 A 中的序号,只需要判断集合 A 中是否包含了集合 B 的所有成员。
【例 9】 根据各国官方语言表,查询官方语言同时包括中文和英文的国家。部分数据如下:
Country |
Language |
China |
Chinese |
UK |
English |
Singapore |
English |
Singapore |
Malay |
Singapore |
Chinese |
Singapore |
Tamil |
Malaysia |
Malay |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from Language") |
/查询官方语言表 |
3 |
=A2.group(Country) |
/按国家分组 |
4 |
=A3.select(~.(Language).contain("Chinese","English")) |
/使用 A.contain() 函数判断当前国家的语言是否包含中文和英文。 |
5 |
=A4.(Country) |
/取出国家列表 |
A5的执行结果如下:
Member |
Singapore |
8. 定位主键值
当需要定位的字段是主键时,可以特殊处理。
【例 10】 在相互关联的产品表和类别表中,查询产品类别未出现在类别表中的有哪些。产品表和类别表的关系如下:
【SPL 脚本】
A |
B |
|
1 |
=connect("db") |
/连接数据库 |
2 |
=A1.query("select * from Category").keys(ID) |
/查询类别表,并设置主键为 ID |
3 |
=A1.query("select * from Product") |
/查询产品表 |
4 |
=A3.select(A2.pfind(CategoryID)==0) |
/使用 A.pfind() 函数在类别表中查找主键等于类别 ID 的序号,返回 0 说明不存在。在产品表中选出类别 ID 不存在的记录。 |
A4的执行结果如下:
ID |
Name |
CategoryID |
… |
12 |
German cheese |
… |
|
26 |
Spun sugar |
9 |
… |
9. 定位前 N 名 / 后 N 名
定位前 N 名 / 后 N 名是比较常见的计算。例如查询班级数学前三名,查询入职时间最短的五名员工等等。这一节要来介绍如何获取前 N 名 / 后 N 名成员所在的序号。
【例 11】 根据股市交易表,统计上证指数 2019 年收盘价最高的三天,相对前日的涨幅。部分数据如下:
Date |
Open |
Close |
Amount |
2019/12/31 |
3036.3858 |
3050.124 |
2.27E11 |
2019/12/30 |
2998.1689 |
3040.0239 |
2.67E11 |
2019/12/27 |
3006.8517 |
3005.0355 |
2.58E11 |
2019/12/26 |
2981.2485 |
3007.3546 |
1.96E11 |
2019/12/25 |
2980.4276 |
2981.8805 |
1.9E11 |
… |
… |
… |
… |
【SPL 脚本】
A |
B |
|
1 |
=file("000001.csv").import@ct() |
/导入数据文件 |
2 |
=A1.select(year(Date)==2019) |
/选出 2019 年的记录 |
3 |
=A2.ptop(-3, Close) |
/使用 A.ptop() 函数取出股市收盘价最高三天的记录序号,-3 表示从大到小取前三。如果是正整数表示从小到大取。 |
4 |
=A3.run(~=A2(~).Amount/A2(~+1).Amount-1) |
/循环用每天交易量和前日交易量计算涨幅 |
A3的执行结果如下:
Member |
154 |
156 |
157 |
A4的执行结果如下:
Member |
-0.0278 |
-0.0139 |
0.0112 |
《SPL CookBook》中还有更多相关计算示例。
英文版