Python案例详解如何用Pandas做数据分组重采样(附实战代码)
📚 目录导读
- 为什么需要数据分组重采样?
- Pandas核心函数与概念速览
- 案例实战:电商订单数据的分组重采样
- 1 数据准备与清洗
- 2 按日/周/月重采样单列数据
- 3 按“地区+时间”双重分组重采样
- 4 自定义聚合函数的方法
- 高频踩坑与性能优化技巧
- 常见问题QA
- 总结与延伸阅读
为什么需要数据分组重采样?
在数据分析工作中,原始日志数据通常具备两个特征:高频率和非结构化,服务器每分钟生成数千条访问日志,或电商平台每秒记录订单变动,直接分析这种细粒度数据会导致:

- 计算量过大,模型训练效率低下
- 难以发现宏观趋势(如周环比、月同比)
- 可视化图表过于杂乱
分组重采样(Grouped Resampling)正是为解决此类问题而生,它允许你按时间维度(如小时、天、周)将数据“压缩”,同时保留分组(如地区、用户类型)的差异性,Pandas 的 resample() 方法结合 groupby(),能优雅地完成这一任务。
核心概念:重采样本质是“时间轴上的分组聚合”,而分组重采样则是“时间轴 + 类别轴”的二维聚合。
Pandas核心函数与概念速览
在动手写代码前,先理解以下关键组件:
| 函数/属性 | 作用 | 典型参数 |
|---|---|---|
pd.to_datetime() |
将字符串/数值转为时间戳 | format, unit |
DataFrame.set_index() |
将时间列设为索引(重采样必要条件) | inplace, drop |
.resample() |
按时间频率分组,返回 DatetimeIndexResampler 对象 |
rule (如 'D', 'W', 'M') |
.groupby() |
按类别分组,返回 GroupBy 对象 |
level=0 或列名 |
.agg() |
指定多个聚合函数 | 字典或列表 |
重要规则:resample() 默认作用于 DatetimeIndex,因此需先用 set_index() 将时间字段设为行索引。
案例实战:电商订单数据的分组重采样
1 数据准备与清洗
假设你是一家电商公司的数据分析师,手头有一份 orders.csv 文件,包含以下字段:
order_id,order_time,city,payment_amount,payment_type
1,2025-01-05 14:32:00,北京,299.0,微信
2,2025-01-05 05:15:00,上海,159.0,支付宝
...
模拟生成1000条随机数据(实际工作中直接读取CSV即可):
import pandas as pd
import numpy as np
# 生成示例数据
dates = pd.date_range(start='2025-01-01', end='2025-03-31', freq='2h')
sample = pd.DataFrame({
'order_time': np.random.choice(dates, 1000),
'city': np.random.choice(['北京', '上海', '广州', '深圳'], 1000),
'payment_amount': np.round(np.random.uniform(50, 500, 1000), 2),
'payment_type': np.random.choice(['微信', '支付宝', '银行卡'], 1000)
})
sample.to_csv('orders.csv', index=False)
数据清洗步骤:
df = pd.read_csv('orders.csv')
# 关键:将订单时间转为datetime并设为索引
df['order_time'] = pd.to_datetime(df['order_time'])
df.set_index('order_time', inplace=True)
df 的索引是时间戳,可以开始重采样。
2 按日/周/月重采样单列数据
需求:计算每天的总支付金额。
daily_revenue = df['payment_amount'].resample('D').sum()
print(daily_revenue.head())
代码说明:
'D'代表按天(日历日)重采样,其他常用规则:'H'(小时)、'W'(周)、'M'(月)、'Q'(季度)。- 可同时聚合多列:
df[['payment_amount', 'order_id']].resample('W').agg({'payment_amount': 'sum', 'order_id': 'count'})
输出示例:
order_time
2025-01-01 12543.18
2025-01-02 9876.45
...
思考题:若想统计每周的订单数量,应使用 .count() 还是 .size()?
count()排除 NaN,size()包含所有行,若数据无缺失,两者等价。
3 按“地区+时间”双重分组重采样
这是核心需求:既要按城市分组,又要按时间压缩。
先 groupby 再 resample(推荐)
# 先按城市分组,再对每组时间序列重采样
grouped_resampled = df.groupby('city').resample('W')['payment_amount'].sum()
print(grouped_resampled.head(10))
输出结构:
city order_time
北京 2025-01-05 7654.32
2025-01-12 8901.45
...
先 resample 再 groupby(不常用)
# 先按周重采样,再按城市分组聚合
resampled_then_grouped = df.resample('W').groupby('city')['payment_amount'].sum()
两种方法的区别:
- 方法一更直观,分组在前,时间在后,便于理解。
- 方法二适合需要先“对齐时间轴”的场景(如处理不规则时间间隔)。
实战建议:大多数情况下使用方法一,如果数据量极大(>100万行),方法二因
resample先压缩数据,内存占用更小。
4 自定义聚合函数的方法
当内置函数(sum, mean, std)无法满足需求时,可使用 apply() 或 agg()。
需求示例:计算每周每个城市的支付金额中位数和支付方式种类数。
def custom_agg(group):
return pd.Series({
'median_amount': group['payment_amount'].median(),
'payment_type_cnt': group['payment_type'].nunique()
})
result = df.groupby('city').resample('W').apply(custom_agg)
print(result.head())
输出:
median_amount payment_type_cnt
city order_time
北京 2025-01-05 287.50 3
2025-01-12 312.80 2
注意:apply 性能较慢,若数据量超百万行,优先用 agg 配合 Numpy 函数。
高频踩坑与性能优化技巧
1 常见错误与解决方案
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
TypeError: Only valid with DatetimeIndex |
时间列未设为索引 | 先执行 df.set_index('time_col') |
| 重采样后结果为空 | 时间索引含 NaN 或间隔不规则 | 用 df.index = df.index.dropna() 或 df.asfreq() 填充缺失时间点 |
| 分组后结果混乱 | groupby 与 resample 顺序颠倒 |
确保先 groupby 再 resample |
2 性能加速技巧
-
使用
pipe()链式调用:减少中间变量拷贝。(df .set_index('order_time') .pipe(lambda x: x.groupby('city').resample('W')['payment_amount'].sum()) ) -
禁用
inplace:Pandas 链式操作中inplace会破坏管道。 -
对
groupby列排序:若分组列是字符串,适当排序可提升性能:df.sort_values('city', inplace=True) -
使用
W-MON代替W:默认周结束于周日,若要指定周一作为周起始:.resample('W-MON')
常见问题QA
Q1:重采样频率如何快速记忆?
A:常见缩写规则:
D= Day,H= Hour,T/min= Minute,S= Second- 月:
M(月末),MS(月初);年:Y(年末),YS(年初) - 带偏移:
W-MON(每周一),BM(每月最后一个工作日)
Q2:为什么我的 resample 返回的索引是“区间”而不是具体日期?
A:当你使用 'M' 或 'Q' 时,Pandas 默认返回 PeriodIndex,可通过 label='right' 或 closed='left' 调整,若需严格 DatetimeIndex,可:
df.resample('M', label='right').sum().to_timestamp()
Q3:分组重采样后如何恢复扁平结构?
A:使用 .reset_index() 即可:
result = df.groupby('city').resample('W')['amount'].sum()
flat_result = result.reset_index()
Q4:能否对非数值列重采样?
A:可以,但需用 first()(取首次出现值)、last() 或 ohlc()(金融OHLC数据)。
df['payment_type'].resample('D').first()
总结与延伸阅读
通过上述案例,你已掌握:
- ✅ 时间索引的设置与清洗
- ✅ 单一时间维度的
resample用法 - ✅ 结合
groupby实现“城市 × 时间”双重聚合 - ✅ 自定义聚合函数及性能优化技巧
延伸方向:
- 进阶:学习
pd.Grouper与groupby混合使用,处理更复杂的多级索引。 - 可视化:将重采样结果与
matplotlib或plotly结合,绘制时间序列折线图。 - 大数据:对于超过内存的数据,使用
pyspark的groupby+window函数替代。
希望这篇指南能将你从“手动处理日志”的泥潭中解救出来,让数据分析变得高效且优雅,如果你在实际应用中遇到特殊的时间分组需求(如“每周四至下周三”等),欢迎在评论区提问,我会持续补充更多案例。