詳解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
"""


怎么樣,是不是即簡單又強大呢?

 

 
 


免責聲明!

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



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