详解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
"""
怎么样,是不是即简单又强大呢?
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏