楔子
這一次我們來用pandas實現一下SQL中的窗口函數,所以也會介紹關於SQL窗口函數的一些知識,以下SQL語句運行在PostgreSQL上。
數據集
select * from sales_data
-- 字段名分別是:saledate(銷售日期)、product(商品)、channel(銷售渠道)、amount(銷售金額)
/*
2019-01-01 桔子 淘寶 1864
2019-01-01 桔子 京東 1329
2019-01-01 桔子 店面 1736
2019-01-01 香蕉 淘寶 1573
2019-01-01 香蕉 京東 1364
2019-01-01 香蕉 店面 1178
2019-01-01 蘋果 淘寶 511
2019-01-01 蘋果 京東 568
2019-01-01 蘋果 店面 847
2019-01-02 桔子 淘寶 1923
2019-01-02 桔子 京東 775
2019-01-02 桔子 店面 599
2019-01-02 香蕉 淘寶 1612
2019-01-02 香蕉 京東 1057
2019-01-02 香蕉 店面 1580
2019-01-02 蘋果 淘寶 1345
2019-01-02 蘋果 京東 564
2019-01-02 蘋果 店面 1953
2019-01-03 桔子 淘寶 729
2019-01-03 桔子 京東 1758
2019-01-03 桔子 店面 918
2019-01-03 香蕉 淘寶 1879
2019-01-03 香蕉 京東 1142
2019-01-03 香蕉 店面 731
2019-01-03 蘋果 淘寶 1329
2019-01-03 蘋果 京東 1315
2019-01-03 蘋果 店面 1956
*/
移動分析和累計求和
這里我們需要說一下什么是窗口函數,窗口函數和聚合函數類似,都是針對一組數據進行分析計算;但不同的是,聚合函數是將一組數據匯總成單個結果,窗口函數是為每一行數據都返回一個匯總后的結果
我們用一張圖來說明一下:

可以看到:聚合函數會將同一個組內的多條數據匯總成一條數據,但是窗口函數保留了所有的原始數據。
窗口函數也被稱為聯機分析處理(OLAP)函數,或者分析函數(Analytic Function)。
我們以 SUM 函數為例,比較這兩種函數的差異。
select sum(amount) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
10970
*/
-- 我們說一旦出現了聚合函數,那么select后面的字段要么出現在聚合函數中,要么出現在group by字句中
-- 但對於窗口函數則不需要
select saledate, product, sum(amount) over() as sum_amount
from sales_data
where saledate = '2019-01-01'
/*
2019-01-01 桔子 10970
2019-01-01 桔子 10970
2019-01-01 桔子 10970
2019-01-01 香蕉 10970
2019-01-01 香蕉 10970
2019-01-01 香蕉 10970
2019-01-01 蘋果 10970
2019-01-01 蘋果 10970
2019-01-01 蘋果 10970
*/
OVER 關鍵字表明 SUM 是一個窗口函數;括號內為空表示將所有數據作為整體進行分析。
查詢結果返回了所有的記錄,並且 SUM 聚合函數為每條記錄都返回了相同的匯總結果。
從上面的示例可以看出,窗口函數與其他函數的不同之處在於它包含了一個 OVER 子句;OVER 子句用於定義一個分析數據的窗口。完整的窗口函數定義如下:
window_function ( expression ) OVER (
PARTITION BY ...
ORDER BY ...
frame_clause
)
其中,window_function 是窗口函數的名稱;expression 是窗口函數操作的對象,可以是字段或者表達式;OVER 子句包含三個部分:分區(PARTITION BY)、排序(ORDER BY)以及窗口大小(frame_clause)
在介紹這些組成之前,我們先來看看上面那個例子使用pandas如何實現:
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine("postgres://postgres:zgghyys123@localhost:5432/postgres")
df = pd.read_sql("select saledate, product, amount from sales_data where saledate = '2019-01-01'",engine)
# 這個實現起來顯然很容易
df["amount"] = df["amount"].sum()
print(df)
"""
saledate product amount
0 2019-01-01 桔子 10970
1 2019-01-01 桔子 10970
2 2019-01-01 桔子 10970
3 2019-01-01 香蕉 10970
4 2019-01-01 香蕉 10970
5 2019-01-01 香蕉 10970
6 2019-01-01 蘋果 10970
7 2019-01-01 蘋果 10970
8 2019-01-01 蘋果 10970
"""
下面來看看這些選項的作用
分區(PARTITION BY)
OVER 子句中的 PARTITION BY 選項用於定義分區,作用類似於 GROUP BY 分組;如果指定了分區選項,窗口函數將會分別針對每個分區單獨進行分析。
select saledate, product, sum(amount) over(partition by product) as sum_amount
from sales_data
where saledate = '2019-01-01'

我們看到窗口函數會針對partition by后面字段進行分區,相同的分為一個區,然后對每個分區里面的值進行計算。我們按照product進行分區,那么所有值為"桔子"的分為一區,那么它的sum_amount就是所有product為"桔子"的amount之和,同理蘋果、香蕉也是如此。
我們看到窗口函數,雖然也用到了聚合,但是它並不需要group by,因為字段的數量和原來保持一致。只是針對partition by后面的字段進行分區,然后對每一個區使用聚合得到一個值,然后給該分區的所有記錄都添上這么一個值。
現在再回來看開始的例子,saledate='2019-01-01'的記錄有10條,那么select sum(amount) from sale_data saledate='2019-01-01'得到的數據只有一條,也就是所有的amount之和。而select sum(amount) over() from sale_data saledate='2019-01-01',我們說由於over()里面是空的,所以相當於整體只有一個分區,這個分區就是整個篩選出來的數據集,那么還是計算所有的amount之和,但是返回的是10條,和原來的數據行數保持一致。
並且窗口函數不需要group by,前面可以直接加上指定的字段,還是那句話,它不改變數據集的大小,而是在聚合之后給原來的每一條記錄都添上這么一個值。但是普通的聚合就不行了,如果select指定了其它字段,那么這些字段必須出現在聚合函數、或者group by字句中,並且計算完之后數據行數會減少(除非group by后面的字段都不重復,但如果不重復的話,我們一般也不會用它來group by)
然后我們看一下如何使用pandas來實現
df = pd.read_sql("select saledate, product, amount from sales_data where saledate = '2019-01-01'",engine)
# pandas實現SQL的聚合函數和窗口函數都使用groupby函數
groupby = df.groupby(by=["product"])
# 如果后面調用了agg,那么等價於SQL的聚合函數。如果是transform,那么就等價於SQL的窗口函數
df["sum_amount"] = groupby["amount"].transform("sum")
print(df)
"""
saledate product amount sum_amount
0 2019-01-01 桔子 1864 4929
1 2019-01-01 桔子 1329 4929
2 2019-01-01 桔子 1736 4929
3 2019-01-01 香蕉 1573 4115
4 2019-01-01 香蕉 1364 4115
5 2019-01-01 香蕉 1178 4115
6 2019-01-01 蘋果 511 1926
7 2019-01-01 蘋果 568 1926
8 2019-01-01 蘋果 847 1926
"""
# 雖然順序不同,但是結果是一致的。
partition by后面可以指定多個字段,比如:
select saledate, product, amount, sum(amount) over(partition by saledate, product) as sum_amount
from sales_data
/*
2019-01-01 桔子 1329 4929
2019-01-01 桔子 1736 4929
2019-01-01 桔子 1864 4929
2019-01-01 蘋果 568 1926
2019-01-01 蘋果 511 1926
2019-01-01 蘋果 847 1926
2019-01-01 香蕉 1178 4115
2019-01-01 香蕉 1573 4115
2019-01-01 香蕉 1364 4115
2019-01-02 桔子 775 3297
2019-01-02 桔子 1923 3297
2019-01-02 桔子 599 3297
2019-01-02 蘋果 1953 3862
2019-01-02 蘋果 564 3862
2019-01-02 蘋果 1345 3862
2019-01-02 香蕉 1057 4249
2019-01-02 香蕉 1612 4249
2019-01-02 香蕉 1580 4249
2019-01-03 桔子 729 3405
2019-01-03 桔子 1758 3405
2019-01-03 桔子 918 3405
2019-01-03 蘋果 1956 4600
2019-01-03 蘋果 1329 4600
2019-01-03 蘋果 1315 4600
2019-01-03 香蕉 1879 3752
2019-01-03 香蕉 1142 3752
2019-01-03 香蕉 731 3752
*/
我們看到,partition by后面指定了saledate、product,那么相當於按照sale、product進行分區,相同的分為一區。然后對每一個分區里面的amount進行求和,然后給該分區里面的所有的行都添上求和之后的值。所以2019-01-01 桔子對應的sum_amount是4929,因為所有2019-01-01 桔子 對應的amount加起來是5929,然后給這個分區對應的每條記錄都添上4929這個值。同理對於其它的記錄也是同樣的道理。
對於pandas而言,只需要再groupby中多指定一個字段即可
df = pd.read_sql("select saledate, product, amount from sales_data",engine)
groupby = df.groupby(by=["saledate", "product"])
df["sum_amount"] = groupby["amount"].transform("sum")
print(df)
"""
saledate product amount sum_amount
0 2019-01-01 桔子 1864 4929
1 2019-01-01 桔子 1329 4929
2 2019-01-01 桔子 1736 4929
3 2019-01-01 香蕉 1573 4115
4 2019-01-01 香蕉 1364 4115
5 2019-01-01 香蕉 1178 4115
6 2019-01-01 蘋果 511 1926
7 2019-01-01 蘋果 568 1926
8 2019-01-01 蘋果 847 1926
9 2019-01-02 桔子 1923 3297
10 2019-01-02 桔子 775 3297
11 2019-01-02 桔子 599 3297
12 2019-01-02 香蕉 1612 4249
13 2019-01-02 香蕉 1057 4249
14 2019-01-02 香蕉 1580 4249
15 2019-01-02 蘋果 1345 3862
16 2019-01-02 蘋果 564 3862
17 2019-01-02 蘋果 1953 3862
18 2019-01-03 桔子 729 3405
19 2019-01-03 桔子 1758 3405
20 2019-01-03 桔子 918 3405
21 2019-01-03 香蕉 1879 3752
22 2019-01-03 香蕉 1142 3752
23 2019-01-03 香蕉 731 3752
24 2019-01-03 蘋果 1329 4600
25 2019-01-03 蘋果 1315 4600
26 2019-01-03 蘋果 1956 4600
"""
在窗口函數中指定 PARTITION BY 選項之后,不需要 GROUP BY 子句也能獲得分組統計信息。如果不指定 PARTITION BY 選項,所有的數據作為一個整體進行分析。
排序(ORDER BY)
OVER 子句中的 ORDER BY 選項用於指定分區內的排序方式,與 ORDER BY 子句的作用類似;排序選項通常用於數據的排名分析。
partition by ... order by ... [asc|desc]
排序也是可以指定多個字段進行排序的,多個字段逗號分隔,order by要在partition by的后面。並且排序也是針對自身所在的分區來的,每個分區的內部進行排序。
我們現在知道了,partition by是根據指定字段分區,然后對每個分區使用前面的函數,忘記說了,over()前面必須是函數,比如:sum(amount) over(),不可以是amount over()。然后order by是根據指定字段,對分區里面的記錄進行排序。可以只指定partition by不指定order by,我們前面已經見過了。當然也可以只指定order by,不指定partition by。我們先來看看只指定order by,不指定partition by的話,會是什么結果。
select amount, sum(amount) over(order by amount) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
511 511
568 1079
847 1926
1178 3104
1329 4433
1364 5797
1573 7370
1736 9106
1864 10970
*/
select amount, sum(amount) over(order by amount desc) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
1864 1864
1736 3600
1573 5173
1364 6537
1329 7866
1178 9044
847 9891
568 10459
511 10970
*/
我們看到實現了累加的效果,我們知道指定partition by,那么根據哪些字段分區是由partition by后面的字段決定的。但如果在不指定partition by、只指定order by的情況下,那么就只有一個分區,這個分區就是全部記錄,然后會根據order by后面的字段對全部記錄進行排序,然后再進行累和(假設是對於sum而言,其它的函數也是類似的),所以第2行的值等於原來第1行的值加上原來第2行的值。
我們目前是按照amount進行order by,而amount沒有重復的,所以是逐行累加。如果我們是根據product進行order by的話會咋樣呢?product是有重復的
select product, amount, sum(amount) over(order by product) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
桔子 1864 4929
桔子 1329 4929
桔子 1736 4929
蘋果 847 6855
蘋果 511 6855
蘋果 568 6855
香蕉 1573 10970
香蕉 1364 10970
香蕉 1178 10970
*/
我們說order by是先排序,這是按照product排序,顯然是按照其拼音首字符的ascii碼進行排序。當然排序不重要,重點是后面的累加。我們看到並沒有逐行累加,而是把product相同的先分別加在一起,得到的結果是:桔子:5929 蘋果: 1926 香蕉:4115,然后再對整體進行累加,所以蘋果的值應該是:5929+1926=7855,同理香蕉的值:5929+1926+4115=11970。
所以這個累加並不是針對每一行來的,而是先把product相同的amount都加在一起,然后對加在一起的值進行累加。並且累加之后,再將累加的的結果添加到對應product的每一條記錄上。而我們上面第一個例子之所以是逐行累加,是因為我們order by指定的是amount,而amount都不重復。
然后我們再來看看pandas如何實現這個邏輯
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
# 如果是實現over(order by)的話,不需要使用groupby
df = df.sort_values(by=["amount"])
df["sum_amount"] = df["amount"].agg("cumsum")
print(df)
"""
product amount sum_amount
6 蘋果 511 511
7 蘋果 568 1079
8 蘋果 847 1926
5 香蕉 1178 3104
1 桔子 1329 4433
4 香蕉 1364 5797
3 香蕉 1573 7370
2 桔子 1736 9106
0 桔子 1864 10970
"""
倒序排序也是可以的
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["amount"], ascending=False)
df["sum_amount"] = df["amount"].agg("cumsum")
print(df)
"""
product amount sum_amount
0 桔子 1864 1864
2 桔子 1736 3600
3 香蕉 1573 5173
4 香蕉 1364 6537
1 桔子 1329 7866
5 香蕉 1178 9044
8 蘋果 847 9891
7 蘋果 568 10459
6 蘋果 511 10970
"""
我們這里amount沒有重復的,所以得到的結果和SQL是一樣的,但如果是product呢?
df = df.sort_values(by=["product"])
df["sum_amount"] = df["amount"].agg("cumsum")
print(df)
"""
product amount sum_amount
0 桔子 1864 1864
1 桔子 1329 3193
2 桔子 1736 4929
6 蘋果 511 5440
7 蘋果 568 6008
8 蘋果 847 6855
3 香蕉 1573 8428
4 香蕉 1364 9792
5 香蕉 1178 10970
"""
我們看到結果和SQL有些不一樣,SQL是先將amount按照product相同的加在一起,然后再進行累加,而pandas依舊是逐行累加。那么如何實現SQL的邏輯呢?
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product"])
df["sum_amount"] = df.groupby(by=["product"])["amount"].transform("sum")
print(df)
"""
product amount sum_amount
0 桔子 1864 4929
1 桔子 1329 4929
2 桔子 1736 4929
6 蘋果 511 1926
7 蘋果 568 1926
8 蘋果 847 1926
3 香蕉 1573 4115
4 香蕉 1364 4115
5 香蕉 1178 4115
"""
# 實現了按照product相同的先加在一起,但是還沒有實現累和
# 蘋果的sum_amount應該是4929 + 1926,香蕉的sum_amount應該是4929 + 1926 + 4115
tmp = df.drop_duplicates(["product"])[["product", "sum_amount"]]
tmp["sum_amount"] = tmp["sum_amount"].cumsum()
print(tmp)
"""
product sum_amount
0 桔子 4929
6 蘋果 6855
3 香蕉 10970
"""
print(
pd.merge(df.drop(columns=["sum_amount"]), tmp, on="product", how="left")
)
"""
product amount sum_amount
0 桔子 1864 4929
1 桔子 1329 4929
2 桔子 1736 4929
6 蘋果 511 6855
7 蘋果 568 6855
8 蘋果 847 6855
3 香蕉 1573 10970
4 香蕉 1364 10970
5 香蕉 1178 10970
"""
所以我們看到over里面的order by實現的就是先排序再累加的效果,只不過這個累加會先根據order by后面的字段中值相同的進行求和,然后再累加。
單獨指定partition by和單獨指定order by我們已經知道了,但如果partition by和order by同時指定的話會怎么樣呢?
select product, amount, sum(amount) over (partition by product order by amount desc) as sum_amount
from sales_data
where saledate = '2019-01-01';
/*
桔子 1864 1864
桔子 1736 3600
桔子 1329 4929
蘋果 847 847
蘋果 568 1415
蘋果 511 1926
香蕉 1573 1573
香蕉 1364 2937
香蕉 1178 4115
*/
我們看到是按照product分區,按照amount排序,但此時依舊出現了累和(我們以前面的聚合是sum為例),但顯然它是在分區內部進行累和。我們知道如果不指定partition by的話,那么order by amount會對整個數據集進行排序,然后進行累和。但是現在指定partition by了,那么會先根據partition by進行分區,然后order by的邏輯還是跟之前一樣,可以認為是在各自的分區內部分別執行了order by。
然后看看pandas如何實現
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
groupby = df.groupby(by=["product"])
df["sum_amount"] = groupby["amount"].transform("cumsum")
print(df)
"""
product amount sum_amount
0 桔子 1864 1864
1 桔子 1329 3193
2 桔子 1736 4929
3 香蕉 1573 1573
4 香蕉 1364 2937
5 香蕉 1178 4115
6 蘋果 511 511
7 蘋果 568 1079
8 蘋果 847 1926
"""
由於排序不一樣,導致每個分區內amount的順序不一樣,但結果是正確的。我們再來看個栗子:
select product, saledate, amount, sum(amount) over (partition by product order by saledate) as sum_amount
from sales_data;
/*
桔子 2019-01-01 1864 4929
桔子 2019-01-01 1329 4929
桔子 2019-01-01 1736 4929
桔子 2019-01-02 599 8226
桔子 2019-01-02 775 8226
桔子 2019-01-02 1923 8226
桔子 2019-01-03 729 11631
桔子 2019-01-03 918 11631
桔子 2019-01-03 1758 11631
蘋果 2019-01-01 847 1926
蘋果 2019-01-01 568 1926
蘋果 2019-01-01 511 1926
蘋果 2019-01-02 564 5788
蘋果 2019-01-02 1953 5788
蘋果 2019-01-02 1345 5788
蘋果 2019-01-03 1956 10388
蘋果 2019-01-03 1329 10388
蘋果 2019-01-03 1315 10388
香蕉 2019-01-01 1573 4115
香蕉 2019-01-01 1178 4115
香蕉 2019-01-01 1364 4115
香蕉 2019-01-02 1612 8364
香蕉 2019-01-02 1580 8364
香蕉 2019-01-02 1057 8364
香蕉 2019-01-03 1879 12116
香蕉 2019-01-03 1142 12116
香蕉 2019-01-03 731 12116
*/
以桔子為例,這個結果像不像我們單獨使用order by的時候所得到的結果呢?我們是按照product分區的,相同的product歸為一個區。然后在各自的分區里面,先通過order by saledate進行排序,再把saledate相同的amount先進行求和,以桔子為例:2019-01-01的amount總和是5929,2019-01-02的amount總和是3297,然后累加,2019-01-02的amount總和就是5929+3297=9226,同理3號的邏輯也是如此。所以我們看到order by的邏輯不變,如果沒有partition by,那么它的作用范圍就是整個數據集、因為此時整體是一個分區;如果有partition by,那么在分區之后,order by的作用范圍就是一個個的分區,就把每一個分區想象成獨立的數據集就行,在各自的分區內部執行order by的邏輯。同理下面的蘋果和香蕉也是一樣的邏輯。
然后使用pandas實現,會稍微麻煩一些:
df = pd.read_sql("select product, saledate, amount from sales_data", engine)
# 執行groupby
groupby = df.groupby(by=["product", "saledate"])
df["sum_amount"] = groupby["amount"].transform("sum")
print(df)
"""
product saledate amount sum_amount
0 桔子 2019-01-01 1864 4929
1 桔子 2019-01-01 1329 4929
2 桔子 2019-01-01 1736 4929
3 香蕉 2019-01-01 1573 4115
4 香蕉 2019-01-01 1364 4115
5 香蕉 2019-01-01 1178 4115
6 蘋果 2019-01-01 511 1926
7 蘋果 2019-01-01 568 1926
8 蘋果 2019-01-01 847 1926
9 桔子 2019-01-02 1923 3297
10 桔子 2019-01-02 775 3297
11 桔子 2019-01-02 599 3297
12 香蕉 2019-01-02 1612 4249
13 香蕉 2019-01-02 1057 4249
14 香蕉 2019-01-02 1580 4249
15 蘋果 2019-01-02 1345 3862
16 蘋果 2019-01-02 564 3862
17 蘋果 2019-01-02 1953 3862
18 桔子 2019-01-03 729 3405
19 桔子 2019-01-03 1758 3405
20 桔子 2019-01-03 918 3405
21 香蕉 2019-01-03 1879 3752
22 香蕉 2019-01-03 1142 3752
23 香蕉 2019-01-03 731 3752
24 蘋果 2019-01-03 1329 4600
25 蘋果 2019-01-03 1315 4600
26 蘋果 2019-01-03 1956 4600
"""
tmp = df.drop_duplicates(["product", "saledate", "sum_amount"])
tmp["sum_amount"] = tmp.groupby(by=["product"])["sum_amount"].transform("cumsum")
print(tmp)
"""
product saledate amount sum_amount
0 桔子 2019-01-01 1864 4929
3 香蕉 2019-01-01 1573 4115
6 蘋果 2019-01-01 511 1926
9 桔子 2019-01-02 1923 8226
12 香蕉 2019-01-02 1612 8364
15 蘋果 2019-01-02 1345 5788
18 桔子 2019-01-03 729 11631
21 香蕉 2019-01-03 1879 12116
24 蘋果 2019-01-03 1329 10388
"""
print(
pd.merge(
df.drop(columns=["sum_amount", "amount"]), tmp, on=["product", "saledate"], how="left"
).sort_values(by=["product", "saledate"])
)
"""
product saledate amount sum_amount
0 桔子 2019-01-01 1864 4929
1 桔子 2019-01-01 1864 4929
2 桔子 2019-01-01 1864 4929
9 桔子 2019-01-02 1923 8226
10 桔子 2019-01-02 1923 8226
11 桔子 2019-01-02 1923 8226
18 桔子 2019-01-03 729 11631
19 桔子 2019-01-03 729 11631
20 桔子 2019-01-03 729 11631
6 蘋果 2019-01-01 511 1926
7 蘋果 2019-01-01 511 1926
8 蘋果 2019-01-01 511 1926
15 蘋果 2019-01-02 1345 5788
16 蘋果 2019-01-02 1345 5788
17 蘋果 2019-01-02 1345 5788
24 蘋果 2019-01-03 1329 10388
25 蘋果 2019-01-03 1329 10388
26 蘋果 2019-01-03 1329 10388
3 香蕉 2019-01-01 1573 4115
4 香蕉 2019-01-01 1573 4115
5 香蕉 2019-01-01 1573 4115
12 香蕉 2019-01-02 1612 8364
13 香蕉 2019-01-02 1612 8364
14 香蕉 2019-01-02 1612 8364
21 香蕉 2019-01-03 1879 12116
22 香蕉 2019-01-03 1879 12116
23 香蕉 2019-01-03 1879 12116
"""
指定窗口大小
指定窗口大小稍微有點復雜,可能需要花點時間來理解,與其說復雜,倒不如說東西有點多。可能開始不理解,但是堅持看完,你肯定會明白的,不要看到一半就放棄了,一定要看完,因為通過后面的例子、以及解釋會對開始的內容進行補充和呼應。
OVER 子句中的 frame_clause 選項用於指定一個移動的窗口。窗口總是位於分區范圍之內,是分區的一個子集。指定了窗口之后,函數不再基於分區進行計算,而是基於窗口內的數據進行計算。窗口選項可以實現許多復雜的計算。例如,累計到當前日期為止的銷量總計,每個月份及其前后各一月(3 個月)的平均銷量等。窗口大小的具體選項如下:
ROWS frame_start
-- 或者
ROWS BETWEEN frame_start AND frame_end
其中,ROWS 表示以行為單位計算窗口的偏移量。frame_start 用於定義窗口的起始位置,可以指定以下內容之一:
- UNBOUNDED PRECEDING,窗口從分區的第一行開始,默認值;
- N PRECEDING,窗口從當前行之前的第 N 行開始;
- CURRENT ROW,窗口從當前行開始。
frame_end 用於定義窗口的結束位置,可以指定以下內容之一:
- CURRENT ROW,窗口到當前行結束,默認值;
- N FOLLOWING,窗口到當前行之后的第 N 行結束。
- UNBOUNDED FOLLOWING,窗口到分區的最后一行結束;
下圖演示了這些窗口選項的作用:

窗口函數依次處理每一行數據,CURRENT ROW 表示當前正在處理的數據;其他的行可以使用相對當前行的位置表示。需要注意的是,窗口的大小不會超出分區的范圍。
窗口函數的選項比較復雜,我們通過一些常見的窗口函數示例來理解它們的作用。常見的窗口函數可以分為以下幾類:聚合窗口函數、排名窗口函數以及取值窗口函數。
許多聚合函數也可以作為窗口函數使用,包括 AVG、SUM、COUNT、MAX 以及 MIN 等。
-- 本來order by amount是按對每個分區內部的記錄進行累加的,當然這里的累加並不是逐行累加,是我們上面說的那樣
-- 只是為了方便,我們就直接說累加了,或者累和也是一樣,因為我們這里是以sum函數為例子
-- 但是我們指定了窗口大小,那么怎么加就由我們指定的窗口大小來決定了,而不是整個分區
select product, amount,
sum(amount) over(partition by product order by amount rows unbounded preceding) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
桔子 1329 1329
桔子 1736 3065
桔子 1864 4929
蘋果 511 511
蘋果 568 1079
蘋果 847 1926
香蕉 1178 1178
香蕉 1364 2542
香蕉 1573 4115
*/
OVER 子句中的 PARTITION BY 選項表示按照product進行分區,ORDER BY 選項表示按照amount進行排序。窗口子句 ROWS UNBOUNDED PRECEDING 指定窗口從分區的第一行開始,默認到當前行結束;也就是分區的第一行從上往下一直加到當前行結束,因為前面的聚合是sum。
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["sum_amount"] = df.groupby(by=["product"])["amount"].transform(lambda x: np.cumsum(x.iloc[:]))
print(df)
"""
product amount sum_amount
1 桔子 1329 1329
2 桔子 1736 3065
0 桔子 1864 4929
6 蘋果 511 511
7 蘋果 568 1079
8 蘋果 847 1926
5 香蕉 1178 1178
4 香蕉 1364 2542
3 香蕉 1573 4115
"""
同理,N PRECEDING 則是從當前行的上N行開始、加到當前行結束
select product, amount,
sum(amount) over(partition by product order by amount rows 2 preceding) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
桔子 1329 1329 -- 其本身
桔子 1736 3065 -- 上面只有1行,沒有兩行,那么有多少加多少 1000+1329
桔子 1864 4929 -- 上兩行加上當前行,1329 + 1736 + 1864
蘋果 511 511
蘋果 568 1079
蘋果 847 1926
香蕉 1178 1178
香蕉 1364 2542
香蕉 1573 4115
*/
看看pandas如何實現
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["sum_amount"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.rolling(window=3, min_periods=1).sum())
print(df)
"""
product amount sum_amount
1 桔子 1329 1329
2 桔子 1736 3065
0 桔子 1864 4929
6 蘋果 511 511
7 蘋果 568 1079
8 蘋果 847 1926
5 香蕉 1178 1178
4 香蕉 1364 2542
3 香蕉 1573 4115
"""
最后再來看看CURRENT ROW,它是最簡單的了
select product, amount,
sum(amount) over(partition by product order by amount rows current row) as sum_amount
from sales_data where saledate = '2019-01-01';
/*
桔子 1329 1329
桔子 1736 1736
桔子 1864 1864
蘋果 511 511
蘋果 568 568
蘋果 847 847
香蕉 1178 1178
香蕉 1364 1364
香蕉 1573 1573
*/
我們看到沒有變化,因為這表示從當前行開始、到當前行,所以就是其本身。所以它單獨使用沒有太大意義,而是和結束位置一起使用。
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["sum_amount"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.rolling(window=1, min_periods=1).sum())
print(df)
"""
product amount sum_amount
1 桔子 1329 1329
2 桔子 1736 1736
0 桔子 1864 1864
6 蘋果 511 511
7 蘋果 568 568
8 蘋果 847 847
5 香蕉 1178 1178
4 香蕉 1364 1364
3 香蕉 1573 1573
"""
如果起始位置和結束位置結合,我們看看會怎么樣?
select product, amount,
-- 計算平均值
avg(amount) over(
-- 表示從當前行的上1行開始,到當前行的下1行結束。當然我們這里數據集比較少,具體指定為多少由你自己決定
-- 然后計算這三行的平均值
partition by product order by amount rows between 1 preceding and 1 following
) as avg_amount
from sales_data where saledate = '2019-01-01';
/*
桔子 1329 1532.5 -- 1329上面沒有值,下面有一個1736,所以直接是(1329+1736) / 2,因為只有兩個值,所是除以2
桔子 1736 1643 -- (上面的1329 + 當前的1736 + 下面的1864) / 3
桔子 1864 1800 -- (上面的1736 + 當前的1864) / 2
蘋果 511 539.5 -- 其它的依次類推
蘋果 568 642
蘋果 847 707.5
香蕉 1178 1271
香蕉 1364 1371.6666666666666667
香蕉 1573 1468.5
*/
對於pandas來講,這種起始位置和結束位置結合的方式,沒有直接的辦法直達,但是依舊可以實現。
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["avg_amount"] = df.groupby(by=["product"])["amount"].transform(lambda x: [np.mean(x[0 if idx - 1 < 0 else idx-1: idx + 2])
for idx in range(len(x))])
print(df)
"""
product amount avg_amount
1 桔子 1329 1532.500000
2 桔子 1736 1643.000000
0 桔子 1864 1800.000000
6 蘋果 511 539.500000
7 蘋果 568 642.000000
8 蘋果 847 707.500000
5 香蕉 1178 1271.000000
4 香蕉 1364 1371.666667
3 香蕉 1573 1468.500000
"""
至於從當前行到窗口的最后一行,就更簡單了,我們就不說了。
所以我們看到可以在窗口中指定大小,方式為:
rows frame_start或者rows between frame_start and frame_end,如果出現了frame_end那么必須要有frame_start,並且是通過between and的形式frame_start的取值為:沒有frame_end的情況下,unbounded preceding
(從窗口的第一行到當前行),n preceding(從當前行的上n行到當前行),current now(從當前行到當前行)。frame_end的取值為:current now
(從frame_start到當前行),n following(從frame_start到當前行的下n行),unbounded following(從frame_start到窗口的最后一行)
使用窗口函數進行分類排名和環比、同比分析
介紹完了窗口函數的概念和語法,以及聚合窗口函數的使用。下面我們繼續討論 SQL 中的排名窗口函數和取值窗口函數,它們分別可以用於統計產品的分類排名和數據的環比/同比分析,然后看看如何使用pandas進行實現。
排名窗口函數
排名窗口函數用於對數據進行分組排名。常見的排名窗口函數包括:
- ROW_NUMBER,為分區中的每行數據分配一個序列號,序列號從 1 開始分配。
- RANK,計算每行數據在其分區中的名次;如果存在名次相同的數據,后續的排名將會產生跳躍。
- DENSE_RANK,計算每行數據在其分區中的名次;即使存在名次相同的數據,后續的排名也是連續的值。
- PERCENT_RANK,以百分比的形式顯示每行數據在其分區中的名次;如果存在名次相同的數據,后續的排名將會產生跳躍。
- CUME_DIST,計算每行數據在其分區內的累積分布。
- NTILE,將分區內的數據分為 N 等份,為每行數據計算其所在的位置。
排名窗口函數不支持動態的窗口大小(frame_clause),而是以整個分區(PARTITION BY)作為分析的窗口。接下來我們通過示例了解一下這些函數的作用。
按照分類進行排名
row_number
select product, amount, row_number() over (partition by product order by amount) as row_number
from sales_data
where saledate = '2019-01-01';
/*
桔子 1329 1
桔子 1736 2
桔子 1864 3
蘋果 511 1
蘋果 568 2
蘋果 847 3
香蕉 1178 1
香蕉 1364 2
香蕉 1573 3
*/
我們使用order by進行排序的時候,除了進行累和之外,很多時候也會通過SQL提供的排名窗口函數為其加上一個排名。比如row_numer(),它是針對每個窗口、然后給里面的記錄生成1 2 3...這樣的序列號。我們先按照amount排個序,然后此時的序列號不就相當於名次了嗎。當然如果沒有partition by,那么就是針對整個數據集進行排名,因為此時只有一個窗口,也就是整個數據集。
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["row_number"] = df.groupby(by=["product"])["amount"].transform(lambda x: range(1, len(x) + 1))
print(df)
"""
product amount row_number
1 桔子 1329 1
2 桔子 1736 2
0 桔子 1864 3
6 蘋果 511 1
7 蘋果 568 2
8 蘋果 847 3
5 香蕉 1178 1
4 香蕉 1364 2
3 香蕉 1573 3
"""
當然如果不排序的話,也是可以使用row_number(),只不過此時的序號就不能代表什么了。
select product, amount, row_number() over (partition by product)
from sales_data
where saledate = '2019-01-01';
/*
桔子 1864 1
桔子 1329 2
桔子 1736 3
蘋果 847 1
蘋果 511 2
蘋果 568 3
香蕉 1573 1
香蕉 1364 2
香蕉 1178 3
*/
-- 如果不指定order by也是可以使用row_number()生成序列號,但還是那句話,此時的序列號只是單純的1 2 3...
-- 它不能代表什么。如果還按照amount排序了,那么我們說此時的row_number()則是對應窗口內部的amount的排名。
rank和dense_rank可以自己嘗試。至於它們以及row_number三者的區別:假設A和B考了100分,那么對於row_number而言,雖然成績一樣,但還是有一個第一、一個第二;而對於rank和dense_rank而言,A和B都是第一。但如果是rank()的話,緊接着考了99分的C只能是第3名,因為前面已經有兩人了,可以認為是按照人數算的;但如果是dense_rank()的話,考了99分的C則是第二名,也就是並列第一看做是一個人,可以認為是按照名次的順序算的,因為A和B都是第一,那么C就該第二了。
percent_rank
至於percent_rank()則是按照排名計算百分比,區間是[0, 1],也就是位於這個區間的什么位置。
select product,
amount,
rank() over (partition by product order by amount) as rank,
dense_rank() over (partition by product order by amount) as dense_rank,
percent_rank() over (partition by product order by amount) as percent_rank
from sales_data
where saledate = '2019-01-01';
/*
桔子 1329 1 1 0
桔子 1736 2 2 0.5
桔子 1864 3 3 1
蘋果 511 1 1 0
蘋果 568 2 2 0.5
蘋果 847 3 3 1
香蕉 1178 1 1 0
香蕉 1364 2 2 0.5
香蕉 1573 3 3 1
*/
-- 關於窗口函數的寫法,我們也可以按照如下方式
-- 由於我們這里的窗口都是(partition by product order by amount),如果是多個窗口
-- 那么就是 window r1 as (...), r2 as (...)
select product,
amount,
rank() over r as rank,
dense_rank() over r as dense_rank,
percent_rank() over r as percent_rank
from sales_data
where saledate = '2019-01-01'
window r as (partition by product order by amount)
;
使用pandas進行計算
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["percent_rank"] = df.groupby(by=["product"])["amount"].transform(lambda x: np.linspace(0, 1, len(x)))
print(df)
"""
product amount percent_rank
1 桔子 1329 0.0
2 桔子 1736 0.5
0 桔子 1864 1.0
6 蘋果 511 0.0
7 蘋果 568 0.5
8 蘋果 847 1.0
5 香蕉 1178 0.0
4 香蕉 1364 0.5
3 香蕉 1573 1.0
"""
利用排名窗口函數可以獲得每個類別中的 Top-N 排行榜
select * from
(select product,
amount,
rank() over (partition by product order by amount) as rank
from sales_data
where saledate = '2019-01-01') as tmp -- 我們說select from也可以當成一張表來用,tmp就是表名
-- 獲取tmp.rank <= 2的,就拿出了每個product對應amount的前兩名,當然我們這里是升序排序的
where tmp.rank <= 2;
/*
桔子 1329 1
桔子 1736 2
蘋果 511 1
蘋果 568 2
香蕉 1178 1
香蕉 1364 2
*/
-- 倒序排序
select * from
(select product,
amount,
rank() over (partition by product order by amount desc) as rank
from sales_data
where saledate = '2019-01-01') as tmp
where tmp.rank <= 2
/*
桔子 1864 1
桔子 1736 2
蘋果 847 1
蘋果 568 2
香蕉 1573 1
香蕉 1364 2
*/
使用pandas進行計算
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["percent_rank"] = df.groupby(by=["product"])["amount"].transform(lambda x: range(1, len(x) + 1))
print(df[df["percent_rank"] <= 2])
"""
product amount percent_rank
1 桔子 1329 1
2 桔子 1736 2
6 蘋果 511 1
7 蘋果 568 2
5 香蕉 1178 1
4 香蕉 1364 2
"""
# 也可以降序,不再演示
累積分布與分片位置
cume_dist
CUME_DIST 函數計算數據對應的累積分布,也就是排在該行數據之前的所有數據所占的比率;取值范圍為大於 0 並且小於等於 1。
select product,
amount,
cume_dist() over (order by amount),
percent_rank() over (order by amount)
from sales_data
/*
蘋果 511 0.037037037037037035 0
蘋果 564 0.07407407407407407 0.038461538461538464
蘋果 568 0.1111111111111111 0.07692307692307693
桔子 599 0.14814814814814814 0.11538461538461539
桔子 729 0.18518518518518517 0.15384615384615385
香蕉 731 0.2222222222222222 0.19230769230769232
桔子 775 0.25925925925925924 0.23076923076923078
蘋果 847 0.2962962962962963 0.2692307692307692
桔子 918 0.3333333333333333 0.3076923076923077
香蕉 1057 0.37037037037037035 0.34615384615384615
香蕉 1142 0.4074074074074074 0.38461538461538464
香蕉 1178 0.4444444444444444 0.4230769230769231
蘋果 1315 0.48148148148148145 0.46153846153846156
蘋果 1329 0.5555555555555556 0.5
桔子 1329 0.5555555555555556 0.5
蘋果 1345 0.5925925925925926 0.5769230769230769
香蕉 1364 0.6296296296296297 0.6153846153846154
香蕉 1573 0.6666666666666666 0.6538461538461539
香蕉 1580 0.7037037037037037 0.6923076923076923
香蕉 1612 0.7407407407407407 0.7307692307692307
桔子 1736 0.7777777777777778 0.7692307692307693
桔子 1758 0.8148148148148148 0.8076923076923077
桔子 1864 0.8518518518518519 0.8461538461538461
香蕉 1879 0.8888888888888888 0.8846153846153846
桔子 1923 0.9259259259259259 0.9230769230769231
蘋果 1953 0.9629629629629629 0.9615384615384616
蘋果 1956 1 1
*/
這個cume_dist和percent_rank有點像,但是percent_rank類似於排名,根據記錄數將[0, 1]等分,然后計算該值在區間中所占的位置。我們以桔子 1329.00 0.5263157894736842 0.5為例,0.5(percent_rank)表示該值正好排在中間的位置。0.5263157894736842(cume_dist)表示有大概百分之52.63的amount小於等於1329。
df = pd.read_sql("select product, amount from sales_data", engine)
df = df.sort_values(by=["amount"])
df = df.assign(
# SQL沒有分區,我們也不分了,只排序即可
# 這里的x就是整個DataFrame
cume_dist=lambda x: np.arange(1, len(x) + 1) / (len(x)),
percent_rank=lambda x: np.linspace(0, 1, len(x))
)
print(df)
"""
product amount cume_dist percent_rank
6 蘋果 511 0.037037 0.000000
16 蘋果 564 0.074074 0.038462
7 蘋果 568 0.111111 0.076923
11 桔子 599 0.148148 0.115385
18 桔子 729 0.185185 0.153846
23 香蕉 731 0.222222 0.192308
10 桔子 775 0.259259 0.230769
8 蘋果 847 0.296296 0.269231
20 桔子 918 0.333333 0.307692
13 香蕉 1057 0.370370 0.346154
22 香蕉 1142 0.407407 0.384615
5 香蕉 1178 0.444444 0.423077
25 蘋果 1315 0.481481 0.461538
1 桔子 1329 0.518519 0.500000
24 蘋果 1329 0.555556 0.538462
15 蘋果 1345 0.592593 0.576923
4 香蕉 1364 0.629630 0.615385
3 香蕉 1573 0.666667 0.653846
14 香蕉 1580 0.703704 0.692308
12 香蕉 1612 0.740741 0.730769
2 桔子 1736 0.777778 0.769231
19 桔子 1758 0.814815 0.807692
0 桔子 1864 0.851852 0.846154
21 香蕉 1879 0.888889 0.884615
9 桔子 1923 0.925926 0.923077
17 蘋果 1953 0.962963 0.961538
26 蘋果 1956 1.000000 1.000000
"""
CUME_DIST 函數計算數據對應的累積分布,也就是排在該行數據之前的所有數據所占的比率;取值范圍為大於 0 並且小於等於 1。
ntile
最后再來看看NTILE,NTILE 函數將分區內的數據分為 N 等份,並計算數據所在的分片位置。
select product,
amount,
ntile(5) over (order by amount)
from sales_data
where saledate = '2019-01-01'
/*
蘋果 511 1
蘋果 568 1
蘋果 847 2
香蕉 1178 2
桔子 1329 3
香蕉 1364 3
香蕉 1573 4
桔子 1736 4
桔子 1864 5
*/
-- 為1的表示對應的amount(銷售額)最低的百分之20的水果
使用pandas來實現
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["amount"])
df["ntile"] = pd.cut(df["amount"], 5, labels=[1, 2, 3, 4, 5])
print(df)
"""
product amount ntile
6 蘋果 511 1
7 蘋果 568 1
8 蘋果 847 2
5 香蕉 1178 3
1 桔子 1329 4
4 香蕉 1364 4
3 香蕉 1573 4
2 桔子 1736 5
0 桔子 1864 5
"""
我們看到此時pandas得到的結果和SQL不一樣,但我個人更傾向於pandas的結果。
取值窗口函數
取值窗口函數用於返回指定位置上的數據。常見的取值窗口函數包括:
- FIRST_VALUE,返回窗口內第一行的數據。
- LAG,返回分區中當前行之前的第 N 行的數據。
- LAST_VALUE,返回窗口內最后一行的數據。
- LEAD,返回分區中當前行之后第 N 行的數據。
- NTH_VALUE,返回窗口內第 N 行的數據。
其中,LAG 和 LEAD 函數不支持動態的窗口大小(frame_clause),而是以分區(PARTITION BY)作為分析的窗口。
lag
我們先來看看lag,lag比較重要。
select product,
amount,
-- lag是返回當前行的第n行數據,我們這里1
-- 所以第2行,返回第1行,第3行返回第2行,依次類推,至於第1行,由於上面沒有東西,所以返回null
lag(amount, 1) over (order by amount)
from sales_data
where saledate = '2019-01-01';
/*
蘋果 511 null
蘋果 568 511
蘋果 847 568
香蕉 1178 847
桔子 1329 1178
香蕉 1364 1329
香蕉 1573 1364
桔子 1736 1573
桔子 1864 1736
*/
-- 我們這里沒有指定分區,所以是整個數據集。如果指定了分區,那么就是每一個分區
使用pandas
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
# 這里我們加大難度,指定分區
df["amount_1"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.shift(1))
print(df)
"""
product amount amount_1
1 桔子 1329 NaN
2 桔子 1736 1329.0
0 桔子 1864 1736.0
6 蘋果 511 NaN
7 蘋果 568 511.0
8 蘋果 847 568.0
5 香蕉 1178 NaN
4 香蕉 1364 1178.0
3 香蕉 1573 1364.0
"""
因此我們如果想計算當前值與上一個值的差值,就可以先向上平移,然后彼此相減,或者還可以計算比率等等。當然計算差值和比率在pandas中還有更簡單的辦法,那就是使用diff函數和pct_change函數,有興趣可以自己了解一下,當然即便不知道這兩個函數,我們也可以使用shift平移、然后再相減、相除的方式實現。
LEAD 函數與 LAG 函數類似,但它返回的是當前行之后的第 N 行數據。
first_value、last_value
select product,
amount,
-- 返回每個窗口的第一個排序之后的amount的值
first_value(amount) over (partition by product order by amount),
-- 返回每個窗口的最后一個排序之后的amount的值
last_value(amount) over (partition by product order by amount)
from sales_data
where saledate = '2019-01-01';
/*
桔子 1329 1329 1329
桔子 1736 1329 1736
桔子 1864 1329 1864
蘋果 511 511 511
蘋果 568 511 568
蘋果 847 511 847
香蕉 1178 1178 1178
香蕉 1364 1178 1364
香蕉 1573 1178 1573
*/
-- 我們看到last_value對應的值貌似不太正常,以桔子為例,難道不應該都是1864嗎?
-- 其實還是我們之前說的,order by排序之后,會有一個累計的效果,比如前面的窗口函數,如果是sum,那么就會累加
-- 比如第一行1000,那么first_value就是1000,last_value也是1000。
-- 但是到了第二行,顯然last_value就是1329了,因為1329是排好序的最后一行(對於當前位置來說),至於first_value在該窗口內部永遠是1000,因為1000是第一個值
-- 所以order by讓人不容易理解的地方就在於,一旦它被指定,那么就不再是對分區進行整體計算了,而是對窗口內部的記錄進行排序、並且進行累計
-- 還是sum,此時不是對整個分區求和、把值添加到分區對應記錄中,而是對分區的記錄的值進行累加
-- 對應到這里的last_value也是一樣的,一開始是1000,但是order by具有累計的效果,至於怎么累計就取決於前面的函數是什么
-- 如果sum就是和下一條記錄的值(amount)1329累加,這里是last_value,那么累計在一起就表現在1329取代1000變成了新的最后一行。
-- 當然我們這里以amount進行的order by,而amount都是不一樣的
-- 如果按照product就不一樣了
select product,
amount,
first_value(amount) over (partition by product order by product),
last_value(amount) over (partition by product order by product)
from sales_data
where saledate = '2019-01-01';
/*
桔子 1864 1864 1736
桔子 1329 1864 1736
桔子 1736 1864 1736
蘋果 847 847 568
蘋果 511 847 568
蘋果 568 847 568
香蕉 1573 1573 1178
香蕉 1364 1573 1178
香蕉 1178 1573 1178
*/
-- 每個分區里面的product都是一樣的, 而我們按照product進行order by的話
-- 那么相同的product應該作為一個整體,所以結果就是上面的那樣
-- 至於first_value和last_value的關系,桔子對應的是first_value大於last_value
-- 蘋果對應的是first_value小於last_value,這是由amount的順序決定的
-- 總之first_value是整個分區的第一條記錄,last_value是整個分區的最后一條記錄
-- 因為order by指定的是product,而product在每個分區里面都是一樣的,而它們是一個整體
-- 有點不好理解,但如果是作用整個分區,order by發揮作用,就是我們上一節說的邏輯
-- 但是像我們通過rows指定窗口大小、以及剛才的leg等等,如果是它們的話,那么就不用考慮order by了
-- 此時的order by只負責排序,計算的話也不是先聚合再累加,而是我們對指定的窗口內的數據進行聚合。
-- 如果是leg,那么order by也只負責排序,怎么計算由leg決定,leg是要求當前數據的上N行的數據。
使用pandas實現
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
# 這里我們加大難度,指定分區
df["first_value"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.iloc[0])
df["last_value"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.iloc[-1])
print(df)
"""
product amount first_value last_value
1 桔子 1329 1329 1864
2 桔子 1736 1329 1864
0 桔子 1864 1329 1864
6 蘋果 511 511 847
7 蘋果 568 511 847
8 蘋果 847 511 847
5 香蕉 1178 1178 1573
4 香蕉 1364 1178 1573
3 香蕉 1573 1178 1573
"""
nth_value
select product,
amount,
-- 返回每個窗口的第2個排序之后的amount的值
nth_value(amount, 2) over (partition by product order by amount)
from sales_data
where saledate = '2019-01-01';
/*
桔子 1329 null
桔子 1736 1736
桔子 1864 1736
蘋果 511 null
蘋果 568 568
蘋果 847 568
香蕉 1178 null
香蕉 1364 1364
香蕉 1573 1364
*/
-- 這個也是一樣,order by也是具有累計的效果
-- 以第一個分區為例,第1行記錄是1000,它沒有第2個元素,所以是null
-- 第2行記錄是1329,那么第2個就是1329
-- 同理第3、第4,第2個也是1329,我們說order by具有累計的效果
所以SQL這一點就很讓人討厭,因為它不是一下針對整個分區來的,而是在每個分區都是從上往下一點一點來的。
df = pd.read_sql("select product, amount from sales_data where saledate = '2019-01-01'", engine)
df = df.sort_values(by=["product", "amount"])
df["nth_value_2"] = df.groupby(by=["product"])["amount"].transform(lambda x: x.iloc[1])
print(df)
# 這才是我們希望看到的結果,pandas則是一下子針對整個分區
"""
product amount nth_value_2
1 桔子 1329 1736
2 桔子 1736 1736
0 桔子 1864 1736
6 蘋果 511 568
7 蘋果 568 568
8 蘋果 847 568
5 香蕉 1178 1364
4 香蕉 1364 1364
3 香蕉 1573 1364
"""
# 如果想實現SQL中的nth_value呢?
df["nth_value_2_sql"] = df.groupby(by=["product"])["amount"].transform(lambda x:
[None if _ < 1 else x.iloc[1] for _ in range(len(x))])
print(df)
"""
product amount nth_value_2 nth_value_2_sql
1 桔子 1329 1736 NaN
2 桔子 1736 1736 1736.0
0 桔子 1864 1736 1736.0
6 蘋果 511 568 NaN
7 蘋果 568 568 568.0
8 蘋果 847 568 568.0
5 香蕉 1178 1364 NaN
4 香蕉 1364 1364 1364.0
3 香蕉 1573 1364 1364.0
"""
總結
以上就是全部內容了,pandas里面的一些函數,我只是使用了,但是沒有詳細介紹。如果不懂的可以網上搜索,或者查看官網、源碼注釋進行學習。
