极具特色的位置运算
集合在计算机中一般都存储为数组形式,其成员天然会有个位置。数据表本质上是记录的集合,也会被存储成数组,作为成员的记录也有位置的概念。而实际应用中确实有很多分析计算都是位置相关的,但 SQL 把数据表处理成无序集合,就会很不方便。
esProc SPL 保留了数组的有序性,并提供了丰富的位置运算方法,可以用简洁易懂的代码轻松实现这类计算需求。
1. 位置序号
在有序数据集中,记录的位置本身就具有业务意义。比如我们要取某股票 2025 年第 5、10、15、20…交易日的数据。简单的思路是在交易日有序的股票数据中,过滤出 2025 年的记录集合,从中取第 5、10、15、20…条记录就可以了。
无序的 SQL 要用窗口函数临时造出个序号才能完成这样的任务,思路有点“绕”:
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY date) AS rn
FROM
stock
WHERE
YEAR(date) = 2025
)
WHERE
rn % 5 = 0;
SPL 支持对记录的位置序号进行计算,可以按照上述简单思路轻松解题:
stock.select(year(date)==2025).select(# % 5==0)
在对数据集循环计算时,# 是当前记录的位置序号。第二个 select 的条件是记录序号 #能被 5 整除,过滤结果自然就是第 5、10、15、20…条记录了。
Python 的源头是常规程序语言,也继承了数组成员位置的概念,相应的代码也较为简洁:
stock_2025 = stock[stock['date'].dt.year == 2025].reset_index(drop=True)
selected_stock = stock_2025[stock_2025.index % 5 == 4]
index 是 Dataframe 对象的默认索引,和 SPL 的 #不同,它是从 0 开始的整数。这个索引是个对象,会占用内存空间。
index 是生成 stock 时自动计算产生的,stock 按年份过滤后必须重置 index,对新的集合重新生成索引,否则 stock_2025.index 得到的还是年份过滤前的位置序号。
而 SPL 中的 #是天然序号,不需要额外的存储空间,也没有重置动作,更加简洁轻便。
Python 还提供了 iloc 函数,能直接用位置取数据:
stock_2025 = stock[stock['date'].dt.year == 2025].reset_index(drop=True)
selected_stock = stock_2025.iloc[4::5]
iloc[4::5] 从整数位置 4(也就是第 5 行)开始,每隔 5 行选取一条数据。
SPL 也可以用类似的方式计算:
stock.select(year(date)==2025).step(5,5)
2. 按条件取位置
条件过滤是常见运算,也就是取得满足一定条件的成员。集合成员有序时,我们有时候还会关心这些满足条件的成员的位置。
比如求某股票上市后,经过多少个交易日涨到了 100 元以上。简单思路是在交易日有序的股票数据中,找出第一个超过 100 元记录的位置序号,就是想要的结果。
SQL 要造个序号,再计算收盘价在 100 元以上记录对应的序号的最小值,思路还是“绕”:
SELECT MIN(rn)
FROM (
SELECT price,
ROW_NUMBER() OVER ( ORDER BY date ) rn
FROM stock)
WHERE price>100
SPL 则轻松完成任务:
stock.pselect(price > 100)
pselect 是 SPL 基于天然位置序号提供的定位函数,用于获得满足条件成员的位置,这里就是第一个大于 100 元记录的位置序号。
Python 使用 Dataframe 索引写出的代码:
stock [stock['price'] > 100].index[0] + 1
相当于常规过滤后取出索引,略有些繁琐。
单纯取得位置经常并不是目标,我们还要用这个位置做进一步的计算。比如:求股价第一次涨到 100 元以上时的涨幅。自然的思路是:找到第一条收盘价大于 100 元的记录位置,取出这个位置和前一个位置上记录的收盘价,两者相减就可以了。
但是,用 SQL 实现就会“绕”得多了:
WITH T AS (
SELECT price,ROW_NUMBER() OVER ( ORDER BY date ) rn,
price - LAG(price) OVER ( ORDER BY date) rising
FROM Stock)
SELECT Rising FROM T WHERE NO = ( SELECT MIN(rn) FROM T WHERE price>100 )
这要把每天的涨幅都计算出来,CTE 语法生成的中间表也要被遍历两次。
SPL 可以按照自然思路实现,代码非常简捷:
p=stock.pselect(price > 4.5)
stock(p).price-stock(p-1).price
SPL 提供了定位计算函数 calc,可以进一步简化这个代码:
stock.calc(stock.pselect(price>100),price-price[-1])
calc 函数针对 stock.pselect(price>100) 得到的位置,计算表达式 price-price[-1]。SPL 支持跨行计算,price[-1] 表示上一行的收盘价。
Python 有位置概念,也能写出类似的代码:
first_index = stock[stock['price'] > 100].index[0]
result =stock.loc[first_index,'price']- stock.loc[first_index - 1,'price']
不过 Python 没有提供类似 calc 的函数,就只能将 loc 写两次。
3. 取最值位置
有序数据集中,最大、最小值记录所在位置也很有业务意义。
比如求股价第一次达到最高价那天的涨幅。简单思路是在有序的股票数据中,找到第一个达到最高价的记录位置,就很容易解题了。
SQL 写起来还是很麻烦:
WITH T AS (
SELECT
price,
ROW_NUMBER() OVER (ORDER BY date) rn,
price - LAG(price) OVER (ORDER BY date) AS rising
FROM
Stock
),
SELECT rising
FROM T
WHERE price = (SELECT MAX(price) FROM T)
ORDER BY rn
LIMIT 1
窗口函数多,遍历次数多,最后还要先排序再取第一条记录。
SPL 提供了返回最大、最小值出现位置的定位函数 pmax 和 pmin,可以写出非常简捷的代码:
stock.calc(stock.pmax(price ), price - price[-1])
pmax 默认是从前向后找第一个最大值位置序号。
最大值成员可能有多个,假设要找所有最大值对应的涨幅,SPL 这样写:
stock.calc(stock.pmax@a(price ), price - price[-1])
@a 选项可以返回所有这些成员的位置。
Python 的函数 idxmax 也可以返回最大值对应的第一条记录位置:
max_idx = stock['price'].idxmax()
increase = stock.loc[max_idx,'price']- stock.loc[max_idx - 1,'price']
像前面代码一样,需要写两次 loc。
如果取所有最大值对应涨幅,Python 的写法就比较啰嗦了:
max_price = stock['price'].max()
max_idxs = stock[stock['price'] == max_price].index
increase=max_price - stock.loc[max_idxs-1,'price'].values
Python 没有提供返回所有最大值位置的函数,只能先求出最大值,过滤出最大值所在记录,最后用 index 求这些记录的位置。
小结一下:SQL 对付有序集合的位置计算非常麻烦,要临时造序号,思路很绕、代码繁琐。Python 有整数索引,也可以按位置取成员,比 SQL 要方便很多,但索引不够自动化,常常要重置,位置运算也不够丰富,类似任务的实现方法也不一致,较复杂些的任务仍显啰嗦,理解难度也大。SPL 位置运算很独特,有简洁的天然序号,和丰富的定位函数,可以轻松实现此类任务。