Loading... # 详解pandas中的rolling 作者:[@古明地盆](https://home.cnblogs.com/u/traditional/) 喜欢这篇文章的话,就点个关注吧,或者去bilibili看看我也行,虽然啥也没有。:[https://space.bilibili.com/12921175](https://space.bilibili.com/12921175) --- 这次我们聊一聊pandas中的rolling函数,这个函数可以被Series对象调用,也可以被DataFrame对象调用,这个函数主要是用来做移动计算的。 举个栗子,假设我们有10天的销售额,我们想每三天求一次总和,比如第五天的总和就是第三天 + 第四天 + 第五天的销售额之和,这个时候我们的rolling函数就派上用场了。 ```Python 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的窗口,窗口从上到下依次滑动,我们画一张图吧: ![](https://img2020.cnblogs.com/blog/1229382/202010/1229382-20201007035726055-1993388676.png) amount.rolling(3)就做了类似于图中的事情,然后调用sum函数,会将每个窗口里面的元素加起来,就得到我们输出的结果。另外窗口的大小可以任意,这里我们以3为例。 除了sum,还可以求平均值、求方差等等,可以进行很多的操作,有兴趣可以自己去尝试一下。当然我们也可以自定义函数: ```Python 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个元素。图上已经画的很清晰了,那么我们如果我们希望元素不够的时候有多少算多少,该怎么办呢?比如:第一个窗口里面的元素之和就是第一个元素,第二个窗口里面的元素之和是第一个元素加上第二个元素。 ```Python 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。 ```Python 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个。所以示意图会变成如下这样: ![](https://img2020.cnblogs.com/blog/1229382/202010/1229382-20201007035734137-871085141.png) ```Python 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个。 ```Python 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函数就变得简单多了。 ```Python 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在格式化时所使用的符号搞混,下面我们来区分一下。 ![](https://img2020.cnblogs.com/blog/1229382/202010/1229382-20201007035742253-2009530239.png) --- 对了,说起这些符号,我还想到了一个asfreq函数,这个函数也非常的有用。 ```Python 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的话,我们也可以进行填充。 ```Python 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上面 ```Python 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 ```python 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 """ ``` 怎么样,是不是即简单又强大呢? Last modification:February 5, 2022 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 如果觉得我的内容对你有用,请随意赞赏