詳解pandas中的rolling
這次我們聊一聊pandas中的rolling函式,這個函式可以被Series物件呼叫,也可以被DataFrame物件呼叫,這個函式主要是用來做移動計算的。
舉個栗子,假設我們有10天的銷售額,我們想每三天求一次總和,比如第五天的總和就是第三天+第四天+第五天
的銷售額之和,這個時候我們的rolling函式就派上用場了。
import pandas as pd | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print(amount.rolling(3).sum()) | |
""" | |
0 NaN # NaN + NaN + 100 |
|
1 NaN # NaN + 100 + 90 | |
2 300.0 # 100 + 90 + 110 | |
3 350.0 # 90 + 110 + 150 | |
4 370.0 # 110 + 150 + 110 | |
5 390.0 # 150 + 110 + 130 | |
6 320.0 # 110 + 130 + 80 | |
7 300.0 # 130 + 80 + 90 | |
8 270.0 # 80 + 90 + 100 | |
9 340.0 # 90 + 100 + 150 | |
dtype: float64 | |
""" |
結果是不是和我們想要的是一樣的呢?amount.rolling(3)相當於建立了一個長度為3的視窗,視窗從上到下依次滑動,我們畫一張圖吧:
amount.rolling(3)就做了類似於圖中的事情,然後呼叫sum函式,會將每個窗口裡面的元素加起來,就得到我們輸出的結果。另外視窗的大小可以任意,這裡我們以3為例。
除了sum,還可以求平均值、求方差等等,可以進行很多的操作,有興趣可以自己去嘗試一下。當然我們也可以自定義函式:
import pandas as pd | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print( | |
amount.rolling(3).agg( | |
# 裡面的引數x就是每個窗口裡面的元素組成的Series物件 |
|
lambda x: sum(x) | |
) | |
) | |
""" | |
0 NaN | |
1 NaN | |
2 300.0 | |
3 350.0 | |
4 370.0 | |
5 390.0 | |
6 320.0 | |
7 300.0 | |
8 270.0 | |
9 340.0 | |
dtype: float64 | |
""" |
我們看到和直接呼叫sum函式的效果是一樣的,當然我們也可以實現其它的邏輯。
此外我們注意到,開始的兩個元素為NaN,這是因為rolling(3)表示從當前位置往上篩選,總共篩選3個元素。圖上已經畫的很清晰了,那麼我們如果我們希望元素不夠的時候有多少算多少,該怎麼辦呢?比如:第一個窗口裡面的元素之和就是第一個元素,第二個窗口裡面的元素之和是第一個元素加上第二個元素。
import pandas as pd | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print( | |
# min_periods表示視窗的最小觀測值 | |
amount.rolling(3, min_periods=1).agg( | |
lambda x: sum(x) | |
) | |
) | |
""" | |
0 100.0 | |
1 190.0 | |
2 300.0 | |
3 350.0 | |
4 370.0 | |
5 390.0 | |
6 320.0 | |
7 300.0 | |
8 270.0 | |
9 340.0 | |
dtype: float64 | |
""" |
我們看到新增一個min_periods引數即可實現,這個引數表示視窗的最小觀測值,即:窗口裡面元素的最小數量,預設它是和視窗的長度相等的。我們視窗長度為3,但指定了min_periods為1,表示元素不夠也沒關係,只要有一個就行。所以如果元素不夠的話,那麼有幾個計算幾個。如果我們指定min_periods為2的話,那麼顯然第一個是NaN,第二個還是190.0,因為窗口裡面的元素個數至少為2。
import pandas as pd | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print( | |
amount.rolling(3, min_periods=2).agg( | |
lambda x: sum(x) | |
) | |
) | |
""" | |
0 NaN | |
1 190.0 | |
2 300.0 | |
3 350.0 | |
4 370.0 | |
5 390.0 | |
6 320.0 | |
7 300.0 | |
8 270.0 | |
9 340.0 | |
dtype: float64 | |
""" |
注意:min_periods必須小於等於視窗長度,否則報錯。
rolling裡面還有一個center引數,預設為False。我們知道rolling(3)
表示從當前元素往上篩選,加上本身總共篩選3個。但如果是將center指定為True的話,那麼是以當前元素為中心,從兩個方向上進行篩選。比如rolling(3, center=True)
,那麼會往上選一個、往下選一個,再加上本身總共是3個。所以示意圖會變成如下這樣:
import pandas as pd | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print( | |
amount.rolling(3, center=True).agg( | |
lambda x: sum(x) | |
) | |
) | |
""" | |
0 NaN | |
1 300.0 | |
2 350.0 | |
3 370.0 | |
4 390.0 | |
5 320.0 | |
6 300.0 | |
7 270.0 | |
8 340.0 | |
9 NaN | |
dtype: float64 | |
""" |
所以在不指定min_periods的情況下,rolling(3, center=True)
會使得開頭出現一個NaN,結尾出現一個NaN。這個時候可能有人好奇了,如果視窗的長度為奇數的話很簡單,比如:長度為9,除去本身之外,再往上選4個、往下選4個,加上本身正好9個。但如果視窗長度為偶數該怎麼辦?比如:長度為8,這個時候會往上選4個、往下選3個,加上本身正好8個。
如果我們想要從上往下篩選的話,該怎麼做呢?比如:視窗長度為3,但我們是希望從當前元素開始往下篩選,加上本身總共篩選3個。
import pandas as pd | |
from pandas.api.indexers import FixedForwardWindowIndexer | |
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150]) | |
print( | |
amount.rolling(FixedForwardWindowIndexer(window_size=3)).agg( | |
lambda x: sum(x) | |
) | |
) | |
""" | |
0 300.0 | |
1 350.0 | |
2 370.0 | |
3 390.0 | |
4 320.0 | |
5 300.0 | |
6 270.0 | |
7 340.0 | |
8 NaN | |
9 NaN | |
dtype: float64 | |
""" |
通過類FixedForwardWindowIndexer即可實現這一點,當然此時就不可以指定center引數了。
agg裡面除了指定單個函式之外,還可以指定一個列表,列表裡面可以有多個函式。會同時執行多個操作,比如求總和的時候還可以求平均,當然此時返回的結果就不再是Series物件了,而是DataFrame物件。
rolling函式還有一個強大的功能,就是它可以對時間進行移動分析。因為pandas本身就是誕生在金融領域,所以非常擅長對時間的操作。
那麼對時間進行移動分析的使用場景都有哪些呢?舉一個筆者在兩年前還是大四的時候,實習時所遇到的問題吧,當時在用pandas做審計,遇到過一個需求就是判斷是否存在30秒內充值次數超過1000次的情況存在(也就是檢測是否存在同時大量充值的情況)
,如果有就把它們找出來。因為每一次充值都對應一條記錄,每條記錄都有一個具體的時間,換句話說就是我要判斷是否存在某個30秒,在這其中出現了超過1000條的記錄。當時pandas不熟,被這個問題直接搞懵了,不過有了rolling函式就變得簡單多了。
import pandas as pd | |
amount = pd.Series([100, 100, 100, 100, 100, 100, 100, 100, 100, 100], | |
index=pd.DatetimeIndex( | |
["2020-1-1", "2020-1-3", "2020-1-4", "2020-1-6", "2020-1-7", | |
"2020-1-9", "2020-1-12", "2020-1-13", "2020-1-14", "2020-1-15"]) | |
) | |
print(amount) | |
""" | |
2020-01-01 100 | |
2020-01-03 100 | |
2020-01-04 100 | |
2020-01-06 100 | |
2020-01-07 100 | |
2020-01-09 100 | |
2020-01-12 100 | |
2020-01-13 100 | |
2020-01-14 100 | |
2020-01-15 100 | |
dtype: int64 | |
""" | |
# 這裡我們還是算3天之內的總和吧, 為了簡單直觀我們把值都改成100 | |
print(amount.rolling("3D").sum()) | |
""" | |
2020-01-01 100.0 | |
2020-01-03 200.0 | |
2020-01-04 200.0 | |
2020-01-06 200.0 | |
2020-01-07 200.0 | |
2020-01-09 200.0 | |
2020-01-12 100.0 | |
2020-01-13 200.0 | |
2020-01-14 300.0 | |
2020-01-15 300.0 | |
dtype: float64 | |
""" |
我們來分析一下,首先rolling("3D")
表示篩選3天之內的,而且如果是對時間進行移動分析的話,那麼要求索引必須是datetime型別。我們先看"2020-01-01",它上面沒有值了,所以是100(此時就沒有NaN了)
;然後是"2020-01-03","2020-01-01"和它之間沒有超過3天,所以加起來總共是200;再看"2020-01-12",由於它只能往上找"2020-01-10"、"2020-01-11"的記錄、然後加在一起,但它的上面是"2020-01-09"顯然超過3天了,所以結果是100(就是它本身)
;最後看"2020-01-14",3天之內的話,應該"2020-01-12"、"2020-01-13",再加上自身的"2020-01-14",所以結果是300。
怎麼樣,是不是很簡單呢?回到筆者當初的那個問題上來,如果是找出30秒內超過1000次的記錄的話,將交易時間設定為索引、直接rolling("30S").count()
、然後找出大於1000的記錄,說明該條記錄往上的第1000條記錄的交易時間和該條記錄的交易時間之差的絕對值不超過30秒(記錄是按照交易時間排好序的)
。至於這30秒內到底交易了多少次,直接將該條記錄的交易時間減去30秒,進行篩選就行了。
所以rolling函式是很強大的,但是當時不知道,傻了吧唧地寫for迴圈一條條遍歷。另外,關於pandas中表示時間的符號估計有人還不太清楚,最主要的是容易和Python datetime在格式化時所使用的符號搞混,下面我們來區分一下。
對了,說起這些符號,我還想到了一個asfreq函式,這個函式也非常的有用。
import pandas as pd | |
amount = pd.Series(list(range(10)), | |
index=pd.date_range("2020-1-1 10:20:00", periods=10, freq="10S") | |
) | |
print(amount) | |
""" | |
2020-01-01 10:20:00 0 | |
2020-01-01 10:20:10 1 | |
2020-01-01 10:20:20 2 | |
2020-01-01 10:20:30 3 | |
2020-01-01 10:20:40 4 | |
2020-01-01 10:20:50 5 | |
2020-01-01 10:21:00 6 | |
2020-01-01 10:21:10 7 | |
2020-01-01 10:21:20 8 | |
2020-01-01 10:21:30 9 | |
Freq: 10S, dtype: int64 | |
""" | |
# 同樣要求索引必須為datetime | |
# 從頭開始每個5秒取樣一次, 如果不存在的話就用NaN填充 | |
print(amount.asfreq("5S")) | |
""" | |
2020-01-01 10:20:00 0.0 | |
2020-01-01 10:20:05 NaN | |
2020-01-01 10:20:10 1.0 | |
2020-01-01 10:20:15 NaN | |
2020-01-01 10:20:20 2.0 | |
2020-01-01 10:20:25 NaN | |
2020-01-01 10:20:30 3.0 | |
2020-01-01 10:20:35 NaN | |
2020-01-01 10:20:40 4.0 | |
2020-01-01 10:20:45 NaN | |
2020-01-01 10:20:50 5.0 | |
2020-01-01 10:20:55 NaN | |
2020-01-01 10:21:00 6.0 | |
2020-01-01 10:21:05 NaN | |
2020-01-01 10:21:10 7.0 | |
2020-01-01 10:21:15 NaN | |
2020-01-01 10:21:20 8.0 | |
2020-01-01 10:21:25 NaN | |
2020-01-01 10:21:30 9.0 | |
Freq: 5S, dtype: float64 | |
""" | |
# 如果是6秒中的話 | |
print(amount.asfreq("6S")) | |
""" | |
2020-01-01 10:20:00 0.0 | |
2020-01-01 10:20:06 NaN | |
2020-01-01 10:20:12 NaN | |
2020-01-01 10:20:18 NaN | |
2020-01-01 10:20:24 NaN | |
2020-01-01 10:20:30 3.0 | |
2020-01-01 10:20:36 NaN | |
2020-01-01 10:20:42 NaN | |
2020-01-01 10:20:48 NaN | |
2020-01-01 10:20:54 NaN | |
2020-01-01 10:21:00 6.0 | |
2020-01-01 10:21:06 NaN | |
2020-01-01 10:21:12 NaN | |
2020-01-01 10:21:18 NaN | |
2020-01-01 10:21:24 NaN | |
2020-01-01 10:21:30 9.0 | |
Freq: 6S, dtype: float64 | |
""" |
如果我們不想要NaN的話,我們也可以進行填充。
import pandas as pd | |
amount = pd.Series(list(range(10)), | |
index=pd.date_range("2020-1-1 10:20:00", periods=10, freq="10S") | |
) | |
# method="bfill", 缺失值用下一個值填充 | |
# method="ffill", 缺失值用上一個值填充 | |
print(amount.asfreq("5S", method="ffill")) | |
""" | |
2020-01-01 10:20:00 0 | |
2020-01-01 10:20:05 0 | |
2020-01-01 10:20:10 1 | |
2020-01-01 10:20:15 1 | |
2020-01-01 10:20:20 2 | |
2020-01-01 10:20:25 2 | |
2020-01-01 10:20:30 3 | |
2020-01-01 10:20:35 3 | |
2020-01-01 10:20:40 4 | |
2020-01-01 10:20:45 4 | |
2020-01-01 10:20:50 5 | |
2020-01-01 10:20:55 5 | |
2020-01-01 10:21:00 6 | |
2020-01-01 10:21:05 6 | |
2020-01-01 10:21:10 7 | |
2020-01-01 10:21:15 7 | |
2020-01-01 10:21:20 8 | |
2020-01-01 10:21:25 8 | |
2020-01-01 10:21:30 9 | |
Freq: 5S, dtype: int64 | |
""" | |
# 或者指定fill_value, 將所有的值都填充為同一個值 | |
print(amount.asfreq("5S", fill_value=999)) | |
""" | |
2020-01-01 10:20:00 0 | |
2020-01-01 10:20:05 999 | |
2020-01-01 10:20:10 1 | |
2020-01-01 10:20:15 999 | |
2020-01-01 10:20:20 2 | |
2020-01-01 10:20:25 999 | |
2020-01-01 10:20:30 3 | |
2020-01-01 10:20:35 999 | |
2020-01-01 10:20:40 4 | |
2020-01-01 10:20:45 999 | |
2020-01-01 10:20:50 5 | |
2020-01-01 10:20:55 999 | |
2020-01-01 10:21:00 6 | |
2020-01-01 10:21:05 999 | |
2020-01-01 10:21:10 7 | |
2020-01-01 10:21:15 999 | |
2020-01-01 10:21:20 8 | |
2020-01-01 10:21:25 999 | |
2020-01-01 10:21:30 9 | |
Freq: 5S, dtype: int64 | |
""" |
注意:rolling和asfreq除了應用在Series物件上之外,還可以用在DataFrame上面
import pandas as pd | |
df = pd.DataFrame({"col1": list(range(10)), "col2": list(range(1, 11)), "col3": ["xx"] * 10}) | |
print(df.rolling(3, min_periods=1).sum()) | |
""" | |
col1 col2 | |
0 0.0 1.0 | |
1 1.0 3.0 | |
2 3.0 6.0 | |
3 6.0 9.0 | |
4 9.0 12.0 | |
5 12.0 15.0 | |
6 15.0 18.0 | |
7 18.0 21.0 | |
8 21.0 24.0 | |
9 24.0 27.0 | |
""" | |
# 會自動對數值型別的列進行計算, 因為sum只能用於數值型別 | |
# 如果是count的話則會應用所有的列, 因為此時和型別無關, 當然結果就是窗口裡面元素的個數啦 | |
print(df.rolling(3, min_periods=1).count()) | |
""" | |
col1 col2 col3 | |
0 1.0 1.0 1.0 | |
1 2.0 2.0 2.0 | |
2 3.0 3.0 3.0 | |
3 3.0 3.0 3.0 | |
4 3.0 3.0 3.0 | |
5 3.0 3.0 3.0 | |
6 3.0 3.0 3.0 | |
7 3.0 3.0 3.0 | |
8 3.0 3.0 3.0 | |
9 3.0 3.0 3.0 | |
""" | |
# 我們同樣可以自定義函式, 如果裡面值傳遞一個函式的話, 那麼預設會將該函式作用在每一列的每一個視窗上 | |
print(df.rolling(3, min_periods=1).agg(lambda x: sum(x))) | |
""" | |
col1 col2 | |
0 0.0 1.0 | |
1 1.0 3.0 | |
2 3.0 6.0 | |
3 6.0 9.0 | |
4 9.0 12.0 | |
5 12.0 15.0 | |
6 15.0 18.0 | |
7 18.0 21.0 | |
8 21.0 24.0 | |
9 24.0 27.0 | |
""" | |
# 但是我們還可以傳遞一個字典, 將每一列應用在不同的函式中, 注意: 字典裡面傳遞的列必須是數值型別 | |
print(df.rolling(3, min_periods=1).agg({"col1": "sum", "col2": "mean"})) | |
""" | |
col1 col2 | |
0 0.0 1.0 | |
1 1.0 1.5 | |
2 3.0 2.0 | |
3 6.0 3.0 | |
4 9.0 4.0 | |
5 12.0 5.0 | |
6 15.0 6.0 | |
7 18.0 7.0 | |
8 21.0 8.0 | |
9 24.0 9.0 | |
""" |
再來看看asfreq
import pandas as pd | |
df = pd.DataFrame({"col1": list(range(10)), "col2": ["xx"] * 10}, index=pd.date_range("2020-1-1", "2020-1-10")) | |
print(df.asfreq("0.5D")) | |
""" | |
col1 col2 | |
2020-01-01 00:00:00 0.0 xx | |
2020-01-01 12:00:00 NaN NaN | |
2020-01-02 00:00:00 1.0 xx | |
2020-01-02 12:00:00 NaN NaN | |
2020-01-03 00:00:00 2.0 xx | |
2020-01-03 12:00:00 NaN NaN | |
2020-01-04 00:00:00 3.0 xx | |
2020-01-04 12:00:00 NaN NaN | |
2020-01-05 00:00:00 4.0 xx | |
2020-01-05 12:00:00 NaN NaN | |
2020-01-06 00:00:00 5.0 xx | |
2020-01-06 12:00:00 NaN NaN | |
2020-01-07 00:00:00 6.0 xx | |
2020-01-07 12:00:00 NaN NaN | |
2020-01-08 00:00:00 7.0 xx | |
2020-01-08 12:00:00 NaN NaN | |
2020-01-09 00:00:00 8.0 xx | |
2020-01-09 12:00:00 NaN NaN | |
2020-01-10 00:00:00 9.0 xx | |
""" |
怎麼樣,是不是即簡單又強大呢?