用Pandas做数据分组重采样与恢复:从入门到实战
📚 目录导读
为什么需要分组重采样?
在金融、物联网、气象等领域,我们经常遇到不规则时间序列数据。

- 多只股票按交易日记录的价格数据
- 多个传感器在不同时间点采集的温度值
- 多个用户的日志行为数据
核心痛点:原始数据频率不一致或有缺失 → 需要统一重采样到固定频率 → 完成分析后可能需要恢复原始频率。
Pandas解决之道:groupby() + resample() + reindex()/asfreq() 三剑客。
问答1:为什么不用
set_index('timestamp').resample()直接处理?
答:当数据包含多个实体(如多只股票)时,必须先按实体分组,再分别重采样,否则会混淆不同实体的时间序列。
核心概念:分组、重采样与恢复
1 分组重采样三步法
# 伪代码流程
df.groupby('entity') \
.resample('W', on='timestamp') \
.agg({'value': 'mean'})
2 恢复操作的本质
恢复不是简单的逆向操作,而是通过插值或填充将降频数据还原到原始时间点,常见方法:
reindex():用原始时间索引重建interpolate():线性/时间插值bfill/ffill:后向/前向填充
3 关键参数说明
| 参数 | 作用 | 示例值 |
|---|---|---|
on |
指定时间列 | 'date' |
level |
多层索引时的层级 | 0或'timestamp' |
closed |
区间闭合方式 | 'left'/'right' |
label |
标签对齐方式 | 'left'/'right' |
问答2:
resample的label参数如何影响结果?
答:以日频→月频为例,label='left'将月度统计结果标记在月初,label='right'标记在月末,这对于后续恢复操作至关重要。
案例1:股票交易数据的日频到周频重采样
1 数据准备
import pandas as pd
import numpy as np
# 生成模拟数据:3只股票,2023年全年交易日数据
dates = pd.date_range('2023-01-01', '2023-12-31', freq='B')
stocks = ['AAPL', 'GOOGL', 'MSFT']
data = []
for stock in stocks:
for date in dates:
data.append({
'stock': stock,
'date': date,
'close': np.random.uniform(100, 500),
'volume': np.random.randint(1e6, 1e8)
})
df = pd.DataFrame(data)
2 分组重采样:计算周度均值
# 按股票分组,每周计算收盘价均值和成交量总和
weekly = df.groupby('stock').resample(
'W', on='date', label='left'
).agg({
'close': 'mean',
'volume': 'sum'
}).reset_index()
print(weekly.head())
# 输出示例:
# stock date close volume
# 0 AAPL 2023-01-02 253.456789 234567890
3 恢复原始日频数据
# 方法1:用原数据索引重建(仅保留已有日期)
original_dates = df.set_index('date').index.unique()
recovered = weekly.set_index('date').groupby('stock').apply(
lambda x: x.reindex(original_dates, method='ffill')
).reset_index()
# 方法2:线性插值恢复
recovered_interp = weekly.set_index('date').groupby('stock').apply(
lambda x: x.reindex(original_dates).interpolate(method='time')
).reset_index()
问答3:为什么恢复时要先
set_index再groupby?
答:reindex操作要求操作对象的时间索引唯一,分组后的子DataFrame时间索引来自resample的结果,必须先确保索引正确。
案例2:传感器数据的分组重采样与恢复
1 数据特点:不规律+多传感器
# 模拟3个传感器,采样间隔5-30秒随机
np.random.seed(42)
sensors = ['sensor_A', 'sensor_B', 'sensor_C']
data = []
for sensor in sensors:
# 每个传感器生成500个随机时间点
base_time = pd.Timestamp('2023-06-01 00:00:00')
times = [base_time + pd.Timedelta(seconds=np.random.randint(5, 30))
for _ in range(500)]
times.sort()
for t in times:
data.append({
'sensor': sensor,
'time': t,
'temperature': 25 + np.random.randn() * 2,
'humidity': 60 + np.random.randn() * 5
})
df = pd.DataFrame(data)
2 重采样到固定1分钟频率
# 每组内按时间排序,重采样到1分钟,取平均值
fixed = df.groupby('sensor', group_keys=False).apply(
lambda x: x.set_index('time').resample('1T').mean()
).reset_index()
3 恢复原始时间戳(保持原始值)
# 关键技巧:记录原始时间戳
df['original_time'] = df['time'] # 保存原始时间
# 恢复时按照原始时间戳去匹配最近的重采样值
recovered = df[['sensor', 'original_time']].merge(
fixed, on='sensor', how='left'
)
# 使用pd.merge_asof进行时间近邻匹配
recovered = pd.merge_asof(
df.sort_values('time'),
fixed.sort_values('time'),
by='sensor',
on='time',
direction='nearest'
)
问答4:
merge_asof和直接reindex有什么区别?
答:merge_asof更适合不规则时间匹配,它找到每个原始时间点最近的重采样值,而reindex要求索引完全匹配,对于丢失的原始时间点,merge_asof能智能匹配最接近的值。
常见问题与解答
Q1: 分组重采样后索引混乱怎么办?
解决方案:始终使用reset_index()或group_keys=False参数。
# 错误示例(会出现多重索引)
bad = df.groupby('stock').resample('W', on='date').mean()
# 正确做法
good = df.groupby('stock', group_keys=False).resample('W', on='date').mean()
Q2: 重采样后出现NaN值如何填充?
三种策略:
# 1. 前向填充(适合慢变数据) resampled.ffill() # 2. 线性插值(适合连续数据) resampled.interpolate(method='linear') # 3. 填充为0(适合计数数据) resampled.fillna(0)
Q3: 恢复数据时如何保留原始统计信息?
技巧:同时保存重采样值和原始数据点数量。
resampled = df.groupby('stock').resample('W', on='date').agg(
avg_close=('close', 'mean'),
count=('close', 'count') # 记录每周有多少个交易日
)
最佳实践与性能优化
1 性能提升技巧
# 1. 预先排序(提升groupby性能)
df = df.sort_values(['stock', 'date'])
# 2. 使用categorical数据类型(加速分组)
df['stock'] = df['stock'].astype('category')
# 3. 避免在groupby中使用lambda(改用命名聚合)
resampled = df.groupby('stock').resample('W', on='date').agg(
avg=('close', 'mean'),
total=('volume', 'sum')
)
2 完整流水线示例
def resample_and_recover(df, group_col, time_col, freq='W'):
"""
完整的分组重采样与恢复函数
"""
# Step1: 保存原始时间
df['_orig_time'] = df[time_col]
# Step2: 分组重采样
resampled = df.groupby(group_col, group_keys=False).resample(
freq, on=time_col
).agg({c: 'mean' for c in df.columns if c not in [group_col, time_col, '_orig_time']})
# Step3: 恢复原始时间(最近邻匹配)
recovered = pd.merge_asof(
df.sort_values(time_col),
resampled.reset_index().sort_values(time_col),
by=group_col,
on=time_col,
direction='nearest',
suffixes=('_orig', '_resampled')
)
return recovered.drop('_orig_time', axis=1)
3 注意事项
- 时间时区:确保所有时间列都带时区或都无时区,混用时会导致重采样错误
- 缺失时间:
resample默认会填充缺失时间点,如果不需要,使用closed='left'或自定义base参数 - 大数据量:使用
numba加速插值,或改用Dask进行分布式处理
📌 核心要点总结
- 分组重采样 =
groupby()+resample()+agg() - 数据恢复 ≠ 逆向操作,而是通过
reindex、interpolate或merge_asof实现 - 性能优化:预排序、使用category类型、避免lambda
- 常见坑:时区不一致、多重索引未重置、缺失值处理不当
掌握了这些技巧,你就能自如应对金融时序分析、IoT传感器数据清洗、用户行为日志标准化等场景,建议在真实项目中先在小样本数据上验证逻辑,再放大到全量数据。
最后问答:如果原始数据有重复的时间戳怎么办?
答:在分组重采样前必须去重!可以使用df.groupby([group_col, time_col]).agg('mean').reset_index()先合并重复值,否则重采样时会出现时间索引不唯一错误。