【程序设计】8.4 [表一表] 循环函数

 

8.4 循环函数

即然排列(序表也是排列)都可以看成是序列,那么针对这些对象应该也能使用循环函数了,我们已经用过 new()和 derive() 了,再来试试以前学过的循环函数,继续使用前面用 new() 造出的 100 条记录的序表。

计算这些人的平均身高,最大体重,BMI 最小值。


A

B

1

=100.new(string(~):name,if(rand()<0.5,"Male","Female"):sex,50+rand(50):weight,1.5+rand(40)/100:height)

2

=A1.(height).avg()

=A1.avg(height)

3

=A1.(weight).max()

=A1.max(weight)

4

=A1.min(weight/height/height)

把排列当成序列,可以正常地执行 A.(x) 运算,常常用来取出某个字段值构成的序列。结构化数据有多个字段,很容易构成一些有业务意义的表达式,所以基于排列的聚合函数会经常直接用某个表达式来计算(这里的 B2、B3、A4)。

针对结构化数据的选出函数也会比针对常规数据时更有业务意义。

找出体重最大的人和 BMI 最小的人:


A

B

1

2

=A1.maxp(weight)

=A1.maxp@a(weight)

3

=A1.minp(weight/height/height)

A2 只找第一个返回,B2 用了 @a 则会出找出使 weight 最大的记录。

这常常是我们更关心的,也就是最大最小值所对应的记录,而不是最大最小值本身。

按条件选出之后的记录还可以做各种集合运算,以及进一步的聚合和选出运算:


A

B

1

2

=A1.select(height>=1.7)

=A1.select(sex=="Female")

3

=A2^B2

=A1.select(height>=1.7 && sex=="Female")

4

=A2\B2

=A1.select(height>=1.7 && sex!="Female")

5

=A2.maxp@a(weight)

=B2.minp(weight/height/height)

6

=A3.avg(height)

=A4.max(weight)

A2 选出身高不低于 1.7 的人,B2 选出女性。A3 计算两者的交集,B3 则用逻辑运算表达和 A3 同样的计算结果,A4 和 B4 类似。A5 和 B5 在选出的排列中进一步做选出,A6 和 B6 在选出的排列上继续做聚合。

排序也是常见的运算:


A

B

1

2

=A1.sort(height)


3

=A1.select(sex=="Female")

=A3.sort(-weight)

4

=A1.sort(height,-weight)


5

=A1.top(-3,weight)

=A1.top(-3;weight)

6

=A1.ranks(height)

=A1.derive(A6(#):hrank)

我们知道,sort 函数缺省的排序方向是从小到大,可以用 @z 实现逆序。但涉及结构化数据常常可能有多个字段排序,而且这些参数的排序方向不同时,这时就没办法只用一个 @z 来控制了。SPL 给出的办法是把参数写成相反数(加个负号),这样继续从小到大排序就会实现原值的逆序了。用负号写成相反数的办法,对于字符串和日期时间类型的数据也都适用。

这样,A2 按身高从低到高排序,B3 将女性按体重从大到小排序。而 A4 则会先按身高从小到大排,身高相同者再按体重从大到小排。

top 函数中使用负数则是另一种表示逆序的办法,相当于取出排序后最后的几个(如果正数则是取前几个)。A5 和 B5 将分别计算最大的三个体重和体重最大的三个人,A5 的计算结果是 3 个数构成的序列,而 B5 返回的 3 条记录构成的排列。SPL 还支持排名函数,A6 的 ranks 函数可以计算出所有人按身高的排名。

我们感觉用 Male 和 Female 表示性别太长了,只要用一个字母 M 和 F 就可以了,以后写比较式时会短一点,可以用 run 函数来做。


A

1

2

>A1.run(sex=left(sex,1))

使用循环函数给字段赋值时,也可以直接使用字段名表示当前记录的字段,不必写成 ~.sex。

derive()可以用来追加字段生成新序表,有时候我们要追加多个字段,有后追加的字段要由先追加的字段计算出来。比如要我们增加 BMI 字段,再根据 BMI 的取值追加一个是否肥胖的标记字段。用 derive() 会写成这样:


A

1

2

=A1.derive(weight/height/height:bmi)

3

=A2.derive(if(bmi>25,"FAT","OK"):flag)

但是,derive 的计算很复杂,需要新建记录和序表并抄录原数据,性能比较差,原则上要尽量少执行。更好的办法是用 derive 和 run 配合完成这个工作:


A

1

2

=A1.derive(weight/height/height:bmi,:flag)

3

=A2.run(flag =if(bmi>25,"FAT","OK"))

在 A2 中一次性把两个字段都追加出来,然后在 A3 中再用 run 来计算 flag 字段的值。只要执行一次 derive,减少新建和抄录序表的动作,运算性能可以提高很多。

从这些例子中再一次体会:结构化数据的多个字段容易构成很多有业务意义的计算表达式,循环函数中针对字段表达式的运算较多,单值序列计算中则相对不常见。

结合学过的读写 Excel 文件的函数,现在我们已经能够使用程序代码对一批 Excel 文件进行合并、过滤、增加计算结果、排序等操作了。

和序列的循环函数类似,排列也会有多层嵌套的情况。

比如我们想计算这群人中男人和女人身高差值最小是多少:


A

B

1

2

=A1.select(sex=="Male")

=A1.select(sex=="Female")

3

=A2.min(B2.min(abs(height-A2.height)))

内层要引用外层循环中的记录字段时,也要写上外层循环函数对应的变量名以表示当前记录,还是不必写 ~。

想要找出哪些对男女使得最小身高差值成立,则要麻烦一些。


A

B

1

2

=A1.select(sex=="Male")

=A1.select(sex=="Female")

3

=A2.conj(B2.([A2.~,~]))

=A3.minp@a(abs(~(1).height-~(2).height))

这里需要用把记录保留下来,形成男女对,然后再用选出函数找到。B3 中的 A3 不再是排列了,省略引用字段名就没有意义了。

排列的成员是有多个字段的记录,信息含量相对丰富,用选出函数得到的结果也是这个记录构成的排列,所以较少再需要 pselect、pmax、pmin 等定位函数返回的位置中蕴含的信息,但涉及到有序计算时就仍然需要获取位置。

现在我们来讨论与次序有关的循环函数,重新生成一个和日期有关的序表。


A

1

=100.new(date(now())-100+~:dt,rand()*100:price)

2

=A1.select(day@w(dt)>1 && day@w(dt)<7)

随机生成某支股票从 100 天之前到今天的价格表,dt 字段为日期,price 为价格。因为周末不交易,生成完数据后把周末日期过滤掉,以后就使用这个 A2。day@w 函数将返回某个日期的星期数,但注意它的返回值,星期天是 1,星期六是 7。因为历史的原因,计算机系统延用了西方人的习惯,星期天是一周的第一天。

首先我们想计算一下每天的涨幅和移动平均价格


A

3

=A2.derive(price-price[-1]:gain)

4

=A2.derive(price[-1:1].avg():mavg)

在排列的循环函数中,可以在字段后加 [±i]引用相邻记录的字段,加 [a:b] 可以引用相邻记录的字段值构成的序列,这些和序列的循环函数都是一样的。

计算这支股票最长连续上涨了多少个交易日:


A

3

=0

4

=A2.(if(price>price[-1],A3+=1,A3=0)).max()

按照自然思维,先填上 0,某天上涨则加 1,不上涨则清 0,可以计算出到某天连续上涨的天数,再取最大值就可以了。

计算股票价格最高那天的涨幅:


A

3

=A2.pmax(price)

4

=A2(A3).price-A2.m(A3-1).price

这里用定位函数 pmax 计算出最大值所在的序号,然后利用这个序号计算出涨幅。如果要考虑最高价可能会有多个日期,则要使用 @a 选项。


A

3

=A2.pmax@a(price)

4

=A3.new(A2(~).dt,A2(~).price-A2.m(~-1).price:gain)

先计算出达到最大值的记录序号构成的序列,再基于这个序列用 new()生成一个两个字段的序列,把日期和涨幅作为字段。new() 在这种情况也可以省略字段名,可以自己看看生成的序表字段名会是什么。

类似地,计算股票价超过 90 元的那些天的平均涨幅:


A

3

=A2.pselect@a(price>90)

4

=A3.new(A2(~).dt,A2(~).price-A2.m(~-1).price:gain)

对于这种针对某个位置再做跨行的计算,SPL 提供了定位计算函数,上面前两段代码还可以写成这样:


A

3

=A2.pmax(price)

4

=A2.calc(A3,price-price[-1])

定位计算函数 calc 允许在非循环函数中使用循环函数中的 ~、#、[] 等语法。


A

3

=A2.pmax@a(price)

4

=A2.calc(A3,price-price[-1])

5

=A3.new(A2(~).dt,A4(#):gain)

calc 函数对于序列也可以使用,但不涉及结构化数据时有业务意义的场景不多,就放在这里再举例子。

【程序设计】 前言及目录

【程序设计】8.3 [表一表] 序表生成

【程序设计】8.5 [表一表] 字段上的计算