SQL窗口函数及Pandas实现
写在前面
窗口函数在处理复杂需求时提供了一种更为简便的数据处理方式,在实际业务中应用非常广泛,也是面试官喜欢重点考察的知识点。
什么是窗口函数?
窗口函数也被称为联机分析函数(OLAP,Online Anallytical Processing)或者分析函数(Analytic Function),窗口指对满足条件的集合进行计算,并对每一行数据返回分析结果,窗口函数的格式如下:
<窗口函数> OVER (partition by <用于分组的列名> order by <用于排序的列名> frame_clause)
1、常用窗口函数
1) 聚合函数:sum()、count()、max()、min()、avg()
2) 排序函数:row_number()、rank()、dense_rank()
3) 分布函数:percent_rank()、cume_dist()
4) 平移函数:lead()、lag()
5) 首尾函数:first_val()、last_val()
2、分区(partition by)
over中partition by类似group by对数据进行分区,此时,窗口函数会对每个分区单独进行分析,如果不指定partition by将会对整体数据进行分析。
3、排序(order by)
over中的order by对分区內的数据进行排序,默认为升序,当order by某个字段中有重复值时会对重复值进行求和,然后对所有数据进行累加。
4、窗口大小(frame_clause)
over中的frame_clause指对分区集合指定一个移动窗口,当指定了窗口大小后函数就不会在分区上进行计算,而是基于窗口大小內的数据进行计算。窗口大小的格式如下:
rows frame_start
or
rows between frame_start and frame_end
其中,rows表示偏移的行数。frame_start表示窗口的起始位置,有三种选项:
- UNBOUNDED PRECEDING,为默认值,表示从第一行开始。
- N PRECEDING,表示从前一行开始,前一行数据缺失则为0 。
- CURRENT ROW,表示从当前行开始。
frame_end表示窗口的结束位置,有三种选项:
- CURRENT ROW为默认值,表示从当前行结束。
- N FOLLOWING,表示当前行后的第N行结束。
- UNBOUNDED FOLLOWING,表示窗口到分区的最后一行结束。
sql中的默认选项为:rows between UNBOUNDED PRECEDING AND CURRENT ROW,表示统计从第一行至当前记录行。
rows between 1 PRECEDING AND 1 FOLLOWING,表示当前行和前一行及后面一行聚合,多用于近N月的数据统计。
rows between current row and UNBOUNDED FOLLOWING,表示当前行及后面所有行。
为什么要使用窗口函数
在实际业务中我们经常会遇到需要对数据结果进行额外的统计,例如在计算各部门员工薪资后新增一列为公司整体薪资,又或者对各部门薪资水平进行排序、计算占比等操作,此时如果不使用窗口函数可能需要对表进行多次的关联才能实现,因此使用窗口函数可以大大简化代码并提升代码的读写性能。
如何使用窗口函数
首先根据窗口函数的定义我们可以知道,窗口函数主要分为了聚合、排序、分布、平移及首尾等类型,对于每一种类型具体的应用场景如下:
聚合函数
聚合函数也可以充当窗口函数,我们需要经常对窗口下的数据集进行聚合统计,也是窗口函数中应用较为广泛的一类。例如,我们需要统计A公司各部门下各员工的销售,并对各部门的最大值、最小值、平均值及计数等。
sql实现
select dept, name, salary,
sum(salary) over(partition by dept) as sum_salary, --各部门员工薪资求和
avg(salary) over(partition by dept) as avg_salary, --各部门员工薪资求平均
min(salary) over(partition by dept) as min_salary, --各部门员工薪资求最小
max(salary) over(partition by dept) as max_salary --各部门员工薪资求最大值
from data
python实现
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
"dept":[company[x] for x in np.random.randint(0,len(company),8)],
"name":["a","b","c","d","e","f","g","h"],
"salary":np.random.randint(10,30,8)
}
)
data['sum_salary'] = data.groupby('dept')['salary'].transform('sum')
data['min_salary'] = data.groupby('dept')['salary'].transform('min')
data['mean_salary'] = data.groupby('dept')['salary'].transform('mean')
data['max_salary'] = data.groupby('dept')['salary'].transform('max')
data['cumsum_salary'] = data.sort_values(['dept','salary']).groupby('dept').salary.cumsum()
data
排序函数
排序函数常用于对分组集或者整体数据进行排名,例如我们需要对各部门员工薪资进行排序,排序函数又可以根据排序方式有以下分类:
1) row_number:对分组內的数据进行"同分不同级"方式排序,不存在序号并列的现象,即使同分时排序也会不同。
2) rank:对分组內的数据进行"同分同级且不紧密"方式排序,当同分时序号相同,其它排序按正常排名进行排序,即1,2,2,4,5。
3) dense_rank:对分组內的数据进行"同分同级且紧密"方式排序,当同分时序号相同,其它排序按下一排名进行排序,即1,2,2,3,4。
sql实现
select dept, name, salary,
row_number(salary) over(partition by dept order by salary desc) as row_number, --对各部门员工薪资按同分不同级方式排序
rank(salary) over(partition by dept order by salary desc) as rank, --对各部门员工薪资按同分同级且紧密方式方式排序
dense_rank(salary) over(partition by dept order by salary desc) as dense_rank --对各部门员工薪资按同分同级且不紧密方式方式排序
from data
python实现
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
"dept":[company[x] for x in np.random.randint(0,len(company),8)],
"name":["a","b","c","d","e","f","g","h"],
"salary":np.random.randint(10,15,8)
}
)
data['row_number'] = data.groupby('dept')['salary'].rank(ascending=False,method='first') #同分不同级
data['rank'] = data.groupby('dept')['salary'].rank(ascending=False,method='min') #"同分同级且不紧密"
data['dense_rank'] = data.groupby('dept')['salary'].rank(ascending=False,method='dense') #"同分同级且紧密"
data
PS:rank函数只能对数值类型进行排列,在使用时需要注意
分布函数
分布函数主要分为两类:percent_rank()和cume_dist()。
percent_rank():指按照排名计算百分比,即该排名位于区间[0,1]的位置,其中区间内第一名为值0,最后一名值为1。其具体公式为:
p
e
r
c
e
n
t
_
r
a
n
k
(
)
=
(
r
a
n
k
−
1
)
/
(
r
o
w
s
−
1
)
percent\_rank() = (rank - 1) / (rows - 1)
percent_rank()=(rank−1)/(rows−1)
cume_dist():指区间內大于等于当前排名的行数占区间内总函数的比例。多用于判断比当前薪资、得分高的用户比例为多少。
sql实现
select dept, name, salary,
percent_rank(salary) over(partition by dept order by salary desc) as percent_rank,
cume_dist(salary) over(partition by dept order by salary desc) as cume_dist
from data
python实现
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
"dept":[company[x] for x in np.random.randint(0,len(company),8)],
"name":["a","b","c","d","e","f","g","h"],
"salary":np.random.randint(10,15,8)
}
)
# data.groupby('dept')['salary'].rank(ascending=False,method='first',pct=True)
data['percent_rank'] = (data.groupby('dept')['salary'].rank(ascending=False,method='min')-1) / \
(data.groupby('dept')['salary'].transform('count')-1) #如果分组只有一个记录则数据为na
data['cume_dist'] = data.groupby('dept')['salary'].rank(ascending=False,method='first',pct=True) #可以结合排序函数的方法使用
data
平移函数
分布函数主要分为两类:lead(列名,n)和lag(列名,n),此函数多用于计算指标同比、环比。
lead(列名,n):获取分区內向下平移n行数据。
lag(列名,n):获取分区內向上平移n行数据。
sql实现
select dept, name, salary,
lead(salary,1) over(partition by dept order by salary desc ) as lead,
lag(salary,1) over(partition by dept order by salary desc) as lag
from data
python实现
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
"dept":[company[x] for x in np.random.randint(0,len(company),8)],
"name":["a","b","c","d","e","f","g","h"],
"salary":np.random.randint(10,30,8)
}
)
data['lead'] = data.sort_values(['dept','salary'],ascending=False).groupby('dept')['salary'].shift(-1) # 分区內向下平移一个单位
data['lag'] = data.sort_values(['dept','salary'],ascending=False).groupby('dept')['salary'].shift(1) # 分区內向上平移一个单位
data
首尾函数
分布函数主要分为两类:first_val()和last_val()。
first_val():获取分区內第一行数据。
last_val():获取分区內最后一行数据。
sql实现
select dept, name, salary,
first_val(salary) over(partition by dept order by salary desc ) as first_val,
# 由于窗口函数默认的是第一行至当前行,所以在使用last_val()函数时,会出现分区内最后一行和当前行大小一致的情况,因此我们需要将分区偏移量改为第一行至最后一行。
last_val(salary) over(partition by dept order by salary desc rows between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING) as last_val
from data
python实现
import numpy as np
import pandas as pd
company=["A","B","C"]
data=pd.DataFrame({
"dept":[company[x] for x in np.random.randint(0,len(company),8)],
"name":["a","b","c","d","e","f","g","h"],
"salary":np.random.randint(10,30,8)
}
)
data['first_val'] = data.groupby('dept')['salary'].transform('min')
data['last_val'] = data.groupby('dept')['salary'].transform('max')
data
Q&A
Q1:聚合函数和窗口函数的区别
区别:聚合函数是将多条数据聚合成一行数据,而窗口函数是为每一行数据返回一个结果。
联系:都是对一组数据进行分析,窗口函数可以使用聚合函数作为函数。
当需要对数据结果进行额外的统计时,我们常常需要使用窗口函数。
Q2:SQL的执行顺序
SQL的书写顺序是SELECT、FROM、JOIN、ON、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT,其执行顺序见下图:
在这里我们需要强调一下sql的执行顺序,因为大多数情况下我们是不需要太多考虑sql执行顺序的,但是由于窗口函数的执行顺序位于大多数字段之后,只位于字段ORDER BY之前,因此相当于在执行所有字段生成的临时表基础之上执行的窗口函数的操作,举个例子:
从上图中我们可以看到,第二张图无GROUP BY和第三张有GROUP BY时,最终数据的行数由7条记录变为3条,就是因为窗口函数是基于GROUP BY 字段执行之后的临时表基础上进行的计算,因此在清楚SQL的执行顺序后我们就能很容易理解最后展现的结果了。
作者:数据科学成长笔记
链接:https://www.jianshu.com/p/ef510d69d859
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。