SPL 的定位运算与位置利用

以有序集合为基础,可以延申出多种运算,最简单的是位置访问,即按下标或位置获取记录集合中的成员。与之相对的是定位运算,即按某种方式获取记录集合中成员的位置,比如字段极值所在记录的位置、符合条件的记录的位置、排序后的记录在原集合里的位置等。定位运算种类丰富功能强大,增强并拓展了结构化数据运算的表达能力,熟练掌握定位运算并灵活利用位置,可以大幅简化复杂的计算目标,提高低效的计算性能。

SQL 的基础数据类型(表)没有天然序号,属于无序集合。虽然有 rownum 等伪字段生成临时序号,但限制较多门槛较高,也没有直接可用的定位函数,这些原因导致 SQL 程序员通常没有意识主动利用位置去解决问题。

SPL 的基础数据类型(序表)具有天然序号,属于有序集合。SPL 内置了丰富的定位运算函数,方便 SPL 程序员计算位置并善加利用。

从定位成员开始

定位成员指获得指定成员(记录)在集合中的位置,是最基础的定位运算,SPL 提供了函数 pos 实现定位成员。

例 1,从水果集合中查找成员香蕉的位置,找不到时返回 null:

["Orange","Banana","Apple","Banana"].pos("Banana") //2

通过可选参数和函数选项,还可以扩展函数 pos 的能力,比如从后往前找、数据有序时用二分查找、返回成员多次出现的位置等等,其他定位函数都有类似的功能。

利用定位成员解决问题

定位成员虽然简单,但建立起利用位置的思维并灵活应用,往往能简化复杂的计算目标。

例 2:现有某班级的课程表,以及各位教授根据课程表做出的授课意向表。授课意向表通常有冲突和空缺,即多位教授可能选择了同一课程,或某课程没有教授选择。现在要计算出课程冲突表,以便教务部门下一步调整并确定最终的授课安排表。

课程表 schedule.txt

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

说明:课的概念是具体时间段的科目,比如周二第 3 节的地理课,一般用代码表示,比如 luc;本例用不到科目的概念。

授课意向表 Intent.txt

Petitti

mif

mig

vif

vig

null

null

Canales

luc

lud

mac

mad

mic

mid

Lucero

lub

luc

lud

lue

mab

mac

Bergamaschi

lua

luf

maa

maf

mia

mif

说明:上面数据没有列头,第 1 列是教授的名字,后面每列都是课程意向,null 表示空缺。

要计算课程冲突,重点是查出每节课对应的教授名单,只用条件查询很难算出理想结果,但配合 pos 函数,先算出课程表里是否存在某教授,后续就容易多了。SPL 代码:


A

B

1

=file("Intent.txt").import()

/ 教授课程意向表

2

=A1.new(#1:professor,~.array().to(2,).select(~):codeArray)

/ 生成新序表,第一列是教授名,第二列是课程列表(集合)

3

=file("schedule.txt").import@t().conj(~.array())

/ 将所有课程先横向再纵向合并成一个集合

4

=A3.(A2.select(codeArray.pos(A3.~)).(professor))

/ 利用位置计算出课程冲突。

5

=create(Monday,Tuesday,Wednesday,Thursday,Friday).record(A4.(~.concat@c()))

/ 将冲突数据呈现成形象的课程冲突表。

A4:循环 A3 的课程集合,使用 pos 函数在授课意向中查找当前课程,选出每节课对应的教授集合,教授集合可能包含一位或多位教授,也可以为空。

[Bergamaschi,Puebla]

[Bergamaschi,Puebla]

[Bergamaschi,Puebla]

A5:将 A4 的课程冲突数据按先横再纵的顺序填入课程表,多位教授对应一个课程时用逗号合并,结果如下:

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

常用的定位运算

很多定位运算以结构化数据为基础,比如 pmax、pmin,获得最大值、最小值所在的位置;ptop 定位前 N 名或后 N 名所在的位置;pselect,获得满足条件的记录所在的位置。灵活应用这些定位运算,可以利用位置简化复杂的结构化数据计算问题。

例 3:根据某股票指数,计算出收盘价最高的那一天的增长率。部分数据如下:

Date

Open

Close

Amount

2019-12-31

3036

3050

2.27E+11

2019-12-30

2998

3040

2.67E+11

2019-12-27

3006

3005

2.58E+11

2019-12-26

2981

3007

1.96E+11

2019-12-25

2980

2981

1.91E+11

2019-12-24

2983

3040

1.92E+11

2019-12-23

2910

2991

1.99E+11

如果不利用位置,通常的办法是先求出每条记录的增长率,再用“收盘价最高“来过滤记录。代码如下:


A

1

=file("d:/000001.txt").import@t().sort(Date)

2

=A1.derive(Close/Close[-1]-1:rate)

3

=A2.select(Close==A1.max(Close))

这么做比较直观,但要计算每条记录的增长率,性能不算最佳。

想要提高性能,应当先找到收盘价最高的记录,只计算这条记录的增长率,pmax 函数可以方便地实现这一目标:


A

1

=file("d:/000001.txt").import@t().sort(Date)

2

=A1.pmax(Close)

3

=A1(A2).Close/A1(A2-1).Close-1

收盘价最高的日期可能不止一天,pmax@a 可以求出多个最大值所在的位置,循环这些位置,可以求出多个增长率:


A

1

=file("d:/000001.txt").import@t().sort(Date)

2

=A1.pmax@a(Close)

3

=A2.new(A1(~).Date, A1(~).Close/A1(~-1).Close -1)

利用前 N 名或后 N 名所在的位置,也可以简化问题并提高性能。例 4:计算收盘价最高的三天的成交量的涨幅。


A

1

=file("d:/000001.txt").import@t().sort(Date)

2

=A1.ptop(-3, Close)

3

=A2.new(A1(~).Date, A1(~).Amount / A1(~-1).Amount)

类似地,可以利用满足查询条件的记录所在的位置来解题。例 5:收盘价涨幅大于 1.03% 的那些交易日,当天成交量的涨幅是多少?


A

1

=file("d:/000001.txt").import@t().sort(Date)

2

=A1.pselect@a(Close/Close[-1]>1.03)

3

=A2.new(A1(~).Date, A1(~).Amount/A1(~-1).Amount)

定位运算的搭档

定位运算的结果经常是位置集合,循环计算位置集合的每一个成员在 SPL 中很常见,利用 SPL 的基本语法和函数可以完成循环计算,但因为循环的主体是位置集合,而不是常见的记录集合,导致代码不够直观,也稍显复杂。比如例 3 中循环处理多个收盘价最高的位置,最后的代码是:A2.new(A1(~).Date, A1(~).Amount / A1(~-1).Amount)

为了方便循环处理位置集合,SPL 为定位运算搭配了 calc 函数,用循环记录集合的形式代替循环位置集合,循环变量改为更直观的当前记录,代码也因此简化。例 3 可以简写成:A1.calc(A2,[Date,Close/Close[-1]-1])

函数 calc 可以配合各类定位运算,例 4 例 5 可以简化为(代码一样):A1.calc(A2,[Date,Amount / Amount[-1]])

进一步利用位置

在结构化数据计算中,有时候需要对同一份数据进行两种排序,并在两种排序间互相切换,不利用位置的话很难直接进行切换。SPL 提供了 psort 函数,用来计算排序后的数据在原序中的位置,这就可以在两种排序中方便地进行切换。

例 6:userConsume 表存储电商的大量用户及其消费金额,现在要根据外部参数 p_userID,计算出该用户需要再花多少钱,才能使自己的消费排名上升。这个算法会频繁执行。

数据量较大且频繁执行,可以将 userConsume 表常驻内存。计算某用户花多少钱才能提高排名,即找到该用户所在的记录,再与上一名用户的消费金额进行跨行计算,金额之差即所求目标。难点在于,数据应当按金额逆序排序,这样才能与上一名用户进行跨行计算;但找到指定用户所在的记录,较快的办法是将数据按用户 ID 排序,这样可以用二分法快速查找(也可以用哈希索引等办法,另议)。

在没有 psort 函数之前,很难同时使用两种排序,并在两者之间进行转换,妥协的做法是常驻内存时只按金额排序:


A

1

=connect("orcl").query@x("select userID, amount

from userConsume order by amount")

2

=env(userConsume,A1)

内存查询时就不能用二分法了:


A

1

=userConsume.pselect(userID: p_userID)

2

=userConsume.calc(A1,if(#>1,amount[-1]-amount,0))

函数 psort 的好处是以位置为中介,在两种排序的数据间方便地进行切换。原理如下:

原数据 UserConsume:

100

300

200

用 psort 计算排序后的数据在原序中的位置,oPos=userConsume.psort():

1

3

2

用 psort 的结果生成新序数据,index=userConsume(oPos):

100

200

300

用 psort 的结果计算新序的位置在原序中对应的数据:oPos(3)=300

使用 psort 函数后,就可以用两种排序显著提高性能。常驻内存代码改为:


A

B

1

=connect("orcl").query@x("select userID, amount

from userConsume order by amount")


2

=env(userConsume,A1)

原序

3

=env(oPos, userConsume.psort(userID))

排序后的记录在原序中的位置

4

=env(index, userConsume(oPos))

利用位置制造排序后的数据,等价于索引

内存查询时可以使用二分法:


A

B

1

=oPos(index.pselect@b(userID: p_userID))

用排序后的数据进行二分查找,并转为原序位置

2

=userConsume.calc(A1,if(#>1,amount[-1]-amount,0))

在原序中进行相对位置计算

除了上面提到的,SPL 还提供了psegpfind等定位函数,灵活运用这些函数,同样可以简化复杂的计算目标,或提高低效的计算性能。