本文目录导读:

- 目录导读
- 什么是分组滚动(Groupby Rolling)?
- 为什么需要分组滚动?
- 案例准备:数据集与安装环境
- 核心实现:Pandas分组滚动操作全解
- 高级技巧:自定义窗口与边界处理
- 实战案例:销售数据的月度滚动统计
- 常见问题与避坑指南
- 问答环节
Python实战案例:如何用Pandas做数据分组滚动(Groupby Rolling)详细指南
目录导读
- 什么是分组滚动(Groupby Rolling)?
- 为什么需要分组滚动?
- 案例准备:数据集与安装环境
- 核心实现:Pandas分组滚动操作全解
- 高级技巧:自定义窗口与边界处理
- 实战案例:销售数据的月度滚动统计
- 常见问题与避坑指南
- 问答环节
什么是分组滚动(Groupby Rolling)?
在许多数据分析场景中,我们需要对数据按某个类别进行分组,然后在每个分组内对时间序列或顺序数据进行滚动窗口计算(例如移动平均、滚动总和、滚动标准差等),Pandas 提供了 groupby().rolling() 组合方法,能够高效完成这一需求。
分组滚动 = 先分组,再在每个组内做滚动计算,对每个销售员,计算最近3天的平均销售额;或对每只股票,计算5日移动平均线。
注意:分组滚动要求数据已经按时间或顺序排好序,否则计算结果会出现逻辑错误。
为什么需要分组滚动?
| 场景 | 说明 | 实例 |
|---|---|---|
| 金融分析 | 每只股票按日期计算5日均线 | 股票代码分组 + 日K线滚动平均 |
| 销售分析 | 每个门店计算7天滚动销售额 | 门店ID分组 + 日期滚动求和 |
| 传感器数据 | 每个设备计算过去1小时的平均温度 | 设备ID分组 + 时间窗口滚动 |
| 用户行为 | 每个用户计算最近3次登录间隔 | 用户ID分组 + 带偏移量的滚动 |
优点:避免手动为每个组写循环,性能高且代码简洁。
案例准备:数据集与安装环境
我们先创建一个模拟销售数据集,包含多个门店的每日销售记录。
import pandas as pd
import numpy as np
# 设置随机种子
np.random.seed(42)
# 生成日期序列
date_range = pd.date_range('2024-01-01', periods=30, freq='D')
# 创建模拟数据:3个门店,每个门店30天
data = []
for store in ['A', 'B', 'C']:
for d in date_range:
data.append({
'store': store,
'date': d,
'sales': np.random.randint(100, 500)
})
df = pd.DataFrame(data)
print(df.head(10))
输出示例(前10行):
store date sales
0 A 2024-01-01 263
1 A 2024-01-02 224
2 A 2024-01-03 451
3 A 2024-01-04 117
4 A 2024-01-05 190
...
注意:Pandas 版本建议 >= 1.3.0,本案例在 Pandas 2.1.0 上测试通过。
核心实现:Pandas分组滚动操作全解
1 基础语法:groupby().rolling()
基本语法结构:
df.groupby('分组列')[['值列']].rolling(window=n).mean()
关键参数说明:
- 分组列:如
store,决定数据如何划分。 - 值列:需要做滚动的数值列名。
- window:滚动窗口大小(整数表示行数,或时间偏移量字符串如 '7D')。
- 聚合函数:支持
.mean()、.sum()、.std()、.apply()等。
2 单列滚动计算:移动平均
计算每个门店的3天移动平均销售额:
# 必须按分组列和日期排序
df_sorted = df.sort_values(['store', 'date'])
# 计算滚动平均值
df_sorted['rolling_avg_3d'] = (
df_sorted.groupby('store')['sales']
.rolling(window=3)
.mean()
.reset_index(level=0, drop=True) # 去除多余的分组索引
)
print(df_sorted[df_sorted['store'] == 'A'].head(10))
输出:
store date sales rolling_avg_3d
0 A 2024-01-01 263 NaN
1 A 2024-01-02 224 NaN
2 A 2024-01-03 451 312.666667
3 A 2024-01-04 117 264.000000
4 A 2024-01-05 190 252.666667
...
解释:
- 第1、2天由于窗口不足3天,返回
NaN。 - 第3天为前3天 (263+224+451)/3 = 312.67。
3 多列同时滚动计算
假设我们有销售额(sales)和顾客数(customers)两列,需要同时计算滚动平均值:
# 添加顾客数列
df['customers'] = np.random.randint(10, 50, size=len(df))
# 多列滚动
df_sorted2 = df.sort_values(['store', 'date'])
result = (
df_sorted2.groupby('store')[['sales', 'customers']]
.rolling(window=3)
.mean()
.reset_index(level=0, drop=True)
)
df_sorted2[['sales_rolling', 'cust_rolling']] = result
print(df_sorted2.head(10))
高级技巧:自定义窗口与边界处理
1 使用时间偏移窗口
如果日期不连续(例如只工作日有数据),用固定行数窗口可能会有偏差,使用时间偏移量可以解决:
# 设置日期为索引
df_ts = df.set_index('date')
# 按门店分组,使用7天滚动窗口(时间偏移)
rolling_7d = (
df_ts.groupby('store')['sales']
.rolling(window='7D') # 注意:使用字符串 '7D' 而非数字
.mean()
)
print(rolling_7d.reset_index().head(10))
关键点:时间偏移窗口要求日期列是 DatetimeIndex 类型,且每个组内日期已排序。
2 自定义滚动函数
使用 .apply() 实现更复杂的计算,例如滚动相关系数:
def rolling_corr(x):
# 假设需要计算销售额和顾客数的滚动相关系数
if len(x) < 3:
return np.nan
return x['sales'].corr(x['customers'])
result_corr = (
df_sorted2.groupby('store')
.rolling(window=5)
.apply(rolling_corr)
)
3 滚动窗口边界处理
默认情况下,窗口不足时不返回值(NaN),可以通过 min_periods 参数控制:
df.groupby('store')['sales'].rolling(window=5, min_periods=2).mean()
min_periods=2 表示至少需要2个值才能计算,否则返回 NaN。
实战案例:销售数据的月度滚动统计
任务:计算每个门店每月累计至当前日期的滚动总和(即运行总和),按日计算。
# 确保按门店和日期排序
df_sorted = df.sort_values(['store', 'date'])
# 方法1:使用 expanding() 替代 rolling()
df_sorted['cumulative_sales'] = (
df_sorted.groupby('store')['sales']
.expanding()
.sum()
.reset_index(level=0, drop=True)
)
# 方法2:滚动窗口设为全部历史(用 large window 模拟)
# 但 expanding() 更直接
print(df_sorted[df_sorted['store'] == 'A'].head(10))
输出:
store date sales cumulative_sales
0 A 2024-01-01 263 263.0
1 A 2024-01-02 224 487.0
2 A 2024-01-03 451 938.0
...
常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 结果出现大量 NaN | 窗口大小大于分组内数据量 | 检查分组是否合理,或使用 min_periods |
| 结果顺序混乱 | 没有对数据排序 | 先 sort_values(['分组列', '时间列']) |
报错 IndexingError |
滚动后索引层级混乱 | 使用 .reset_index(level=0, drop=True) 清理 |
| 性能慢(大数据) | 默认窗口计算效率低 | 尝试使用 Numba 加速或选择合适的窗口类型 |
特别提醒:在 groupby().rolling() 之后,结果会多出一个分组索引层级,如果直接赋值回原 DataFrame,需要先 drop 掉多余的索引层级,否则会报 ValueError。
问答环节
Q1:分组滚动比直接循环快吗?
A:通常快 10~50 倍,Pandas 对 groupby().rolling() 做了 C 级别优化,尤其是当窗口大小固定时,建议用 apply() 代替 Python 原生循环。
Q2:如果分组内有缺失值怎么办?
A:Pandas 默认在计算时跳过 NaN(取决于具体聚合函数)。mean() 会自动忽略 NaN,但如果想要精确控制,可以使用 min_periods 和 skipna=False。
Q3:时间偏移窗口和固定行数窗口有什么区别?
A:固定行数窗口(如 window=3)只看最近的3行数据,不考虑时间间隔;时间偏移窗口(如 window='3D')按日期向前推算3天,适合不规则时间序列。
Q4:能否对多个分组键同时做滚动?
A:可以,df.groupby(['store', 'product']).rolling(window=3).mean(),但要注意这会生成多层索引。
Q5:滚动窗口能否向前看(未来数据)?
A:默认是向后看(基于过去的数据),如果想看未来数据(未来3天平均」),可以在分组后对时间排序进行反向,或者使用 rolling() 的 center=True 参数(中心对齐窗口)。
Pandas 的 groupby().rolling() 是时间序列分组分析的瑞士军刀,掌握本文的核心语法、时间偏移窗口、以及常见避坑指南,你就能轻松应对 80% 的分组滚动计算场景,实际使用时,务必先排序、注意索引层级处理,并根据数据特性选择合适的窗口类型。