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

怎么样,是不是即简单又强大呢?

posted @ 2020-10-07 03:58  古明地盆  阅读(42783)  评论(12编辑  收藏  举报