如何在GroupBy对象中使用滚动函数

88
我有一个时间序列对象grouped,类型为<pandas.core.groupby.SeriesGroupBy object at 0x03F1A9F0>grouped.sum()可以得到想要的结果,但是我无法将rolling_sum应用于groupby对象。有没有办法对groupby对象应用rolling函数?例如:
x = range(0, 6)
id = ['a', 'a', 'a', 'b', 'b', 'b']
df = DataFrame(zip(id, x), columns = ['id', 'x'])
df.groupby('id').sum()
id    x
a    3
b   12

然而,我希望有类似这样的东西:
  id  x
0  a  0
1  a  1
2  a  3
3  b  3
4  b  7
5  b  12

1
你到底希望对分组对象进行滚动函数的操作是什么(我的意思是用符号写出你想做的数学运算)? - tacaswell
抱歉,我应该表达得更清楚。 - user1642513
所以你想对每个组进行cumsum,然后将整个结果拼接回单个数据框中? - tacaswell
是的,理想情况下应该包括cumsum和任何滚动函数(平均值、总和、标准差)。 - user1642513
5个回答

142

对于遇到这个旧问题的谷歌员工:

关于@kekert在@Garrett的答案中建议使用新版的评论:

df.groupby('id')['x'].rolling(2).mean()

与其使用现在已弃用的

df.groupby('id')['x'].apply(pd.rolling_mean, 2, min_periods=1)

奇怪的是,似乎新的.rolling().mean()方法返回一个多层次索引的序列,首先由group_by列索引,然后是索引本身。而旧的方法只返回由原始df索引单独索引的序列,这也许不太合理,但这使将该系列作为原始数据框的新列添加非常方便。

所以我想我已经找到了一种使用新的rolling()方法并且仍然可以正常工作的解决方案:

df.groupby('id')['x'].rolling(2).mean().reset_index(0,drop=True)

这应该会给你这个系列

0    0.0
1    0.5
2    1.5
3    3.0
4    3.5
5    4.5

你可以将其作为列添加:

df['x'] = df.groupby('id')['x'].rolling(2).mean().reset_index(0,drop=True)

我认为你可以使用.transform而不是reset_index - TMrtSmith
20
如果按多列分组,则此方法将失败。但删除第一个参数(levels)可以解决问题,因为它默认会删除所有级别。 所以代码变成df['x'] = df.groupby('id')['x'].rolling(2).mean().reset_index(drop=True) - Kartik Sreenivasan
14
如果你的分组变量未排序,请使用 groupby(..., sort=False)。这是另一个让人烦恼的细节问题。因为顺序与原始数据框不匹配,所以当将滚动均值作为新列添加时,我得到了非常奇怪的结果。 - Hendy
你能详细说明一下为什么我们应该加上 .rolling(2),也就是为什么这里要设置 window=2 吗?是因为有两个组 'a' 和 'b' 吗? - uniquegino
@KevinWang 是的,我知道window的应用,但我认为原问题中的最后一行是组b所有值的总和(12=3+4+5),即window=3,这让我最困惑——如何使用.rolling(2).mean()在每个组中神奇地提供cumsum需求。我一定漏掉了一些基本的东西,但现在无法想清楚。 - uniquegino
显示剩余3条评论

83

累加求和

直接回答问题,cumsum 方法将生成所需的序列:

In [17]: df
Out[17]:
  id  x
0  a  0
1  a  1
2  a  2
3  b  3
4  b  4
5  b  5

In [18]: df.groupby('id').x.cumsum()
Out[18]:
0     0
1     1
2     3
3     3
4     7
5    12
Name: x, dtype: int64

pandas 按组统计的滚动函数

更一般地,任何滚动函数都可以按如下方式应用于每个组(使用@kekert评论中提到的新的.rolling方法)。请注意,返回类型是一个多重索引的系列,这与以前(已弃用的)pd.rolling_* 方法不同。

In [10]: df.groupby('id')['x'].rolling(2, min_periods=1).sum()
Out[10]:
id
a   0   0.00
    1   1.00
    2   3.00
b   3   3.00
    4   7.00
    5   9.00
Name: x, dtype: float64

为了应用按组滚动计算的函数并按照原始数据框的顺序接收结果,应使用transform:

In [16]: df.groupby('id')['x'].transform(lambda s: s.rolling(2, min_periods=1).sum())
Out[16]:
0    0
1    1
2    3
3    3
4    7
5    9
Name: x, dtype: int64

弃用的方法

供参考,这里是现在已经被弃用的pandas.rolling_mean函数的行为:

In [16]: df.groupby('id')['x'].apply(pd.rolling_mean, 2, min_periods=1)
Out[16]: 
0    0.0
1    0.5
2    1.5
3    3.0
4    3.5
5    4.5

51
pd.rolling_mean已经不再适用于Series,将被移除,请使用df.groupby('id')['x'].rolling(2).mean()代替。 - kekert
df.reset_index().groupby('id', sort=False)['x'].rolling(2, min_periods=1).mean().sort_index(level=1).reset_index(drop=True) 将数据框进行重置后,按'id'分组,计算滚动窗口为2的平均值,最小时间段为1,并按第二级索引排序并重置索引。 - nrcjea001
如果原始索引已经排序,则将df.reset_index()替换为df - nrcjea001

11

这里有另一种通用的方法,使用 pandas 的 expanding 方法。它非常高效,也非常适用于固定窗口的滚动窗口计算,例如时间序列。

# Import pandas library
import pandas as pd

# Prepare columns
x = range(0, 6)
id = ['a', 'a', 'a', 'b', 'b', 'b']

# Create dataframe from columns above
df = pd.DataFrame({'id':id, 'x':x})

# Calculate rolling sum with infinite window size (i.e. all rows in group) using "expanding"
df['rolling_sum'] = df.groupby('id')['x'].transform(lambda x: x.expanding().sum())

# Output as desired by original poster
print(df)
  id  x  rolling_sum
0  a  0            0
1  a  1            1
2  a  2            3
3  b  3            3
4  b  4            7
5  b  5           12

1
你有什么依据证明这是“非常高效”的吗?通常情况下,使用pandas进行任何形式的迭代(例如“transform”或“apply”)都会对性能造成重大影响,与使用向量操作执行相同操作相比(内置的“.sum”,“.rolling”等)。我知道Pandas确实会对迭代循环进行一些预检查,以查看是否可以为您优化它,但通常情况下,如果性能是一个问题,应避免迭代。 - bwest87
1
很抱歉我只能给你一个赞,我正在考虑创建新账户来为这个答案增加更多的信用。这是唯一一个对我有用的多列分组的解决方案,谢谢! - sousben
1
很棒。这可以应用指数移动平均。q['exponential_ave'] = q.groupby('id')['x'].transform(lambda x: x.ewm(com=0.2).mean()) - Darkhan
1
使用expanding和使用rolling的区别是什么? - liang
1
@liang 这篇文章比我能讲得更好,它解释了 Pandas 中扩展窗口和滚动窗口之间的区别。在滚动函数中,窗口大小保持不变,而在扩展函数中,它会改变。也可以参考这个答案。 - Sean McCarthy

4
如果你需要将分组的滚动函数重新分配回原始数据帧,同时保持顺序和分组,可以使用transform函数。
df.sort_values(by='date', inplace=True)
grpd = df.groupby('group_key')
#using center=false to assign values on window's last row
df['val_rolling_7_mean'] = grpd['val'].transform(lambda x: x.rolling(7, center=False).mean())

3
我不确定具体的机制,但是这个方法有效。请注意,返回值只是一个ndarray数组。我认为您可以使用任何累积或“滚动”函数以相同的方式进行应用,并且应该具有相同的结果。
我已经使用cumprod、cummax和cummin进行了测试,它们都返回了一个ndarray数组。我认为pandas足够智能,知道这些函数返回一个系列,因此将函数应用作转换而不是聚合。
In [35]: df.groupby('id')['x'].cumsum()
Out[35]:
0     0
1     1
2     3
3     3
4     7
5    12

编辑:我发现这种语法确实返回了Series:

In [54]: df.groupby('id')['x'].transform('cumsum')
Out[54]:
0     0
1     1
2     3
3     3
4     7
5    12
Name: x

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接