不可或缺的相邻引用

数据分析经常出现跨行计算,比如比上期、比同期、移动平均等等。针对有序数据集实现跨行计算,会涉及集合相邻成员引用的问题。

比如某商家某年 12 个月的销售额已经按月份次序准备好,现在要计算最大月增长额。

SQL 这样写:

sql 复制代码
with sales as (
    select column_value as amount, rownum as month
    from table(sys.odcinumberlist(123,345,321,345,546,542,874,234,543,983,434,897))
),
lagged_sales as (
    select amount, month, 
        lag(amount) over (order by month) as prev
    from sales
)
select max(amount - prev) as max
from lagged_sales;

窗口函数可以引用上月销售额,但要写子查询,代码很啰嗦。而且 SQL 基于无序集合,不能利用数据原有的顺序,每个窗口函数都要单独写 order by。

其实,数据本身总会有个次序,利用这个次序可以方便地表达计算需求。比如这里,数据若按月份有序,只要用当前成员减去上一个成员,差值就是月增长额。如果有一种相邻成员的引用语法,解决类似问题就很轻松了。

esProc SPL 就提供了集合相邻成员引用机制:

ini 复制代码
sales = [123, 345, 321, 345, 546, 542, 874, 234, 543, 983, 434, 897]
sales.(if(#>1,~-~[-1],0)).max()

~ 表示当前成员,# 是当前成员的序号,~[i] 表示和当前成员距离为 i 的成员,这里的 ~[-1] 表示前一个成员,也就是上月销售额。

实际上,if(#>1,-[-1],0)是一个 lambda 函数,外层的 sales.() 是循环函数。SPL 固定使用 ~,# 和 [] 这些符号来表示 lambda 函数的参数。

SPL 的 lambda 函数看起来就是一个普通表达式,语法比传统 lambda 更简洁,传入的参数却更丰富。

Python 的 Series 或 Dataframe 对象直接用 lambda 的话,只能传入当前行这一个参数,不支持序号和相邻引用的语法。要解决这个问题,必须先用 rolling 函数创建滑动窗口对象:

sql 复制代码
result = sales.rolling(window=2).apply(lambda x: x[1] - x[0], raw=True).max()

rolling 创建了大小为 2 的窗口,apply 函数操作会作用于 sales 序列中相邻的两个成员,可以利用 lambda 求这两个成员的差。

但 Python 多了一个创建窗口对象的步骤,略显啰嗦。而且 Python 的 lambda 函数是显示定义的,需要写 lambda 关键字,还要定义传入参数,不如 SPL 简洁。

Python 也可以换个思路:

ini 复制代码
sales=pd.Series([123, 345, 321, 345, 546, 542, 874, 234, 543, 983, 434, 897])
result = (sales-sales.shift(1)).max()

shift 函数把原序列向后移动一个位置生成新序列,再和原序列对位求差值。这样要多计算出一个序列对象,相比之下,还是 SPL 在原序列中引用相邻成员的代码更简单。

Python 还提供了 diff 函数,可以求相邻成员的差:

ini 复制代码
pd.Series([123, 345, 321, 345, 546, 542, 874, 234, 543, 983, 434, 897])
result = sales.diff().max()

diff 支持 periods 参数,能求当前成员与距离为 periods 的成员之差。diff 和类似的 pct_change 等函数都是固定计算,对于没有现成函数可用的计算,只能继续用 rolling 或 shift 来实现。

SPL 就完全没有问题,~[i] 在表达式中可以随意使用,能实现各种各样的相邻计算。

除了相邻成员外,我们还常常要引用相邻的集合。比如还是上面的数据,我们希望计算每个月和前两个月的销售额移动平均值。

SQL 这样写:

sql 复制代码
with sales as (
    select column_value as amount, rownum as month
    from table(sys.odcinumberlist(123,345,321,345,546,542,874,234,543,983,434,897))
)
select month,amount,
    avg(amount) over (order by month rows between 2 preceding and current row) as moving_average
from sales;

SQL 有相邻集合的概念,比如这三个相邻的销售额就组成了一个集合。但 SQL 无法保持住这个集合,要马上聚合,也就只能用 avg、sum、count 等固有聚合函数,如果情况再复杂些,SQL 就不能使用相邻集合计算了。

比如要判断每月和前两个月的销售额是否递增。简单思路是把这三个月销售额组成有序的小集合,再计算这个小集合是否递增。

SQL 没有判断递增的聚合函数,只能这样写:

sql 复制代码
with sales as (
    select column_value as amount, rownum as month
    from table(sys.odcinumberlist(123,345,321,345,546,542,874,234,543,983,434,897))
)
select month,amount,
    amount > lag(amount, 1) over (order by month)  and 
    lag(amount, 1) over (order by month) > lag(amount, 2) over (order by month)   
    as flag
from sales;

结果要用三个 lag 函数,而且,还要三个 order by month,很啰嗦。

SPL 把 ~[i] 扩展成 ~[a,b] 来表示相邻集合,无论是移动平均,还是递增判断,都很容易描述:

ini 复制代码
sales = [123, 345, 321, 345, 546, 542, 874, 234, 543, 983, 434, 897]
sales.(~[-2,0].avg())
sales.(~[-2,0].pselect(~<=~[-1])==null)

~[-2,0] 表示从上上成员开始,到当前成员组成的相邻集合。

想计算滑动平均,就对相邻集合使用 avg 函数。

要判断递增,就用位置计算函数 pselect 在相邻集合中查找,如果没有成员小于等于上一个成员,那就是递增了。pselect 参数中 [-1] 表示相邻集合的当前和上一个成员。

Python 也有相邻集合概念,和 SQL 类似,移动平均这类简单聚合可以轻松写出来:

ini 复制代码
sales = pd.Series([123, 345, 321, 345, 546, 542, 874, 234, 543, 983, 434, 897])
result = sales.rolling(window=3, min_periods=1).mean()

判断递增就麻烦很多,又要借助 lambda 函数了:

ini 复制代码
result = sales.rolling(window=3).apply(lambda x: (x[0] < x[1] and x[1] < x[2]), raw=True)

rolling 滑动窗口是固定大小的,很不灵活。比如希望窗口是从第一个成员到当前成员,就需要用另一个函数 expanding。如果希望窗口是从当前成员到最后一个成员,则需要反转序列。

而 SPL 使用统一的语法,[,0] 表示第一个成员到当前成员,[0,] 表示当前成员到最后一个成员。

对于常见的结构化计算,SPL 相邻引用语法做了简化,可直接使用字段名。比如 sales 表有字段 month、amount 和 rate,求每月 amount*rate 增长额的代码是这样:

css 复制代码
sales.derive(amount*rate-amount[-1]*rate[-1]:increase)

~[-1].amount 直接写成 amount[-1],代码很简洁。

Python 没有为数据表而简化,表名会重复出现:

css 复制代码
sales['increase']=(sales['amount']*sales['rate']).rolling(window=2).apply(lambda x:x[1] - x[0], raw=True)

SPL 相邻引用机制适用于所有集合,当然也包括分组后的子集合。比如 sales 表存储了各门店某年 12 个月的销售额,都是按月份有序的。计算各门店月增长额的代码是这样:

scss 复制代码
=sales.group(store).(~.derive(amount-amount[-1]:increase))

group 函数按照门店分组并保持分组子集,~ 就是当前子集合。针对子集的相邻引用语法和全集一致,两者计算过程也相同。

SQL 没有分组子集,要在窗口函数中增加 partition by,变得更复杂:

sql 复制代码
with monthly_growth as (
    select *,
        ((amount - lag(amount, 1) over (partition by store order by month)) / lag(amount, 1) over (partition by store order by month))  as increase
    from
        sales
)
select *
from monthly_growth
order by store, month;

Python 也有分组子集,用 rolling 配合 groupby 函数就可以:

scss 复制代码
sales['increase'] = sales.groupby('store')['amount'].rolling(window=2).apply(lambda x: x[1] - x[0], raw=True)

语法和不分组时基本一致,不过还要写繁琐的 lambda 函数。

小结一下:SQL 基于无序集合,经常需要用子查询和窗口函数来实现相邻成员之间的计算,代码比较啰嗦。Python 支持有序集合,实现相邻成员引用的优势要大得多,但仍有语法不一致问题,根据情况选择不同的函数,有时还要反转集合。SPL 在有序集合基础上提供相邻引用机制,相邻计算语法简洁、一致,学习和理解也最容易。

esProcSPL是免费的,欢迎下载试用~~

相关推荐
程序员岳焱13 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯19 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响22 分钟前
枚举在实际开发中的使用小Tips
后端
wuhunyu27 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi27 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
程序员清风2 小时前
阿里二面:Kafka 消费者消费消息慢(10 多分钟),会对 Kafka 有什么影响?
java·后端·面试
CodeSheep3 小时前
宇树科技,改名了!
前端·后端·程序员