数据分析经常出现跨行计算,比如比上期、比同期、移动平均等等。针对有序数据集实现跨行计算,会涉及集合相邻成员引用的问题。
比如某商家某年 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是免费的,欢迎下载试用~~