使用pandas實現SQL的窗口函數(附帶窗口函數的詳細講解)


楔子

這一次我們來用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里面的一些函數,我只是使用了,但是沒有詳細介紹。如果不懂的可以網上搜索,或者查看官網、源碼注釋進行學習。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM