不可或缺的相邻引用

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

比如某商家某年 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是免费的,欢迎下载试用~~

相关推荐
Asthenia041227 分钟前
由浅入深解析Redis事务机制及其业务应用-电商场景解决超卖
后端
Asthenia041228 分钟前
Redis详解:从内存一致性到持久化策略的思维链条
后端
Asthenia041228 分钟前
深入剖析 Redis 持久化:RDB 与 AOF 的全景解析
后端
Apifox39 分钟前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
掘金一周1 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
uhakadotcom1 小时前
构建高效自动翻译工作流:技术与实践
后端·面试·github
Asthenia04121 小时前
深入分析Java中的AQS:从应用到原理的思维链条
后端
Asthenia04121 小时前
如何设计实现一个定时任务执行器 - SpringBoot环境下的最佳实践
后端
兔子的洋葱圈2 小时前
【django】1-2 django项目的请求处理流程(详细)
后端·python·django
Asthenia04122 小时前
如何为这条sql语句建立索引:select * from table where x = 1 and y < 1 order by z;
后端