【程序设计】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 函数对于序列也可以使用,但不涉及结构化数据时有业务意义的场景不多,就放在这里再举例子。