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


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

     

     
  • 相关阅读:
    BZOJ4223 : Tourists
    BZOJ3565 : [SHOI2014]超能粒子炮
    BZOJ3499 : PA2009 Quasi-template
    BZOJ3490 : Pa2011 Laser Pool
    BZOJ2828 : 火柴游戏
    BZOJ3070 : [Pa2011]Prime prime power 质数的质数次方
    BZOJ2138 : stone
    BZOJ2167 : 公交车站
    BZOJ1290 : [Ctsc2009]序列变换
    Ural2110 : Remove or Maximize
  • 原文地址:https://www.cnblogs.com/cangqinglang/p/14915959.html
Copyright © 2020-2023  润新知