Pandas 时间序列
问题
如何用 Pandas 处理日期时间数据?重采样和滑动窗口怎么用?
答案
时间序列分析是数据分析的核心场景——日报、周报、月度趋势、同环比、留存分析都离不开日期处理。
日期解析与创建
日期创建与解析
import pandas as pd
# 字符串转日期
df['date'] = pd.to_datetime(df['date_str'])
df['date'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d')
df['date'] = pd.to_datetime(df['date_str'], errors='coerce') # 解析失败变 NaT
# 从多列创建日期
df['date'] = pd.to_datetime(df[['year', 'month', 'day']])
# 创建日期范围
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D') # 每天
dates = pd.date_range(start='2024-01-01', periods=12, freq='MS') # 12个月初
dates = pd.date_range(start='2024-01-01', periods=52, freq='W-MON') # 每周一
# Unix 时间戳转日期
df['date'] = pd.to_datetime(df['timestamp'], unit='s') # 秒级
df['date'] = pd.to_datetime(df['timestamp'], unit='ms') # 毫秒级
日期属性提取
提取日期成分
df['date'] = pd.to_datetime(df['date'])
# 常用日期属性
df['year'] = df['date'].dt.year # 年
df['month'] = df['date'].dt.month # 月
df['day'] = df['date'].dt.day # 日
df['weekday'] = df['date'].dt.dayofweek # 星期(0=周一, 6=周日)
df['week'] = df['date'].dt.isocalendar().week # ISO 周数
df['quarter'] = df['date'].dt.quarter # 季度
df['hour'] = df['date'].dt.hour # 时
df['dayofyear'] = df['date'].dt.dayofyear # 一年中的第几天
# 自定义格式化
df['month_str'] = df['date'].dt.strftime('%Y-%m') # "2024-03"
df['date_str'] = df['date'].dt.strftime('%Y年%m月%d日')
日期过滤
按日期筛选
# 精确日期
df[df['date'] == '2024-03-01']
# 日期范围
df[(df['date'] >= '2024-01-01') & (df['date'] < '2024-04-01')]
# 最近 N 天
latest = df['date'].max()
df[df['date'] >= latest - pd.Timedelta(days=30)]
# 按年月筛选
df[df['date'].dt.year == 2024]
df[df['date'].dt.month.isin([1, 2, 3])] # Q1
日期运算
日期加减与差值
# 日期偏移
df['next_day'] = df['date'] + pd.Timedelta(days=1)
df['prev_week'] = df['date'] - pd.Timedelta(weeks=1)
df['next_month'] = df['date'] + pd.offsets.MonthBegin(1)
# 日期差值
df['days_diff'] = (df['end_date'] - df['start_date']).dt.days
# 距今天数
df['days_ago'] = (pd.Timestamp.now() - df['date']).dt.days
重采样 (resample)
resample 用于改变时间频率,类似于 groupby + 时间维度:
resample 重采样
# 设置日期索引
df = df.set_index('date')
# 日 → 月:月度汇总
monthly = df.resample('ME').agg({
'amount': 'sum', # 月总额
'order_id': 'count' # 月订单数
})
# 日 → 周:周度汇总
weekly = df.resample('W-MON').sum() # 按周一为起始
# 常用频率代码
# 'D' - 日, 'W' - 周, 'ME' - 月末, 'MS' - 月初
# 'QE' - 季末, 'YE' - 年末, 'H' - 小时, 'T' - 分钟
# 上采样(低频 → 高频)+ 填充
monthly.resample('D').ffill() # 月 → 日,前向填充
滑动窗口 (rolling)
rolling 滑动窗口
# 7 日移动平均
df['ma_7'] = df['amount'].rolling(window=7).mean()
# 30 日移动求和
df['sum_30'] = df['amount'].rolling(window=30).sum()
# 最小周期数(窗口内至少有 N 个非 NaN 值才计算)
df['ma_7'] = df['amount'].rolling(window=7, min_periods=3).mean()
# 中心对齐(窗口以当前点为中心)
df['ma_7_center'] = df['amount'].rolling(window=7, center=True).mean()
# 指数加权平均(近期权重更大)
df['ewm'] = df['amount'].ewm(span=7).mean()
同比与环比
同比环比计算
# 准备月度数据
monthly = df.resample('ME')['amount'].sum().reset_index()
# 环比(与上月相比)—— 等价 SQL LAG
monthly['mom'] = monthly['amount'].pct_change() # Month over Month
# 同比(与去年同月相比)
monthly['yoy'] = monthly['amount'].pct_change(periods=12) # Year over Year
# shift 手动计算
monthly['prev_month'] = monthly['amount'].shift(1)
monthly['mom_rate'] = (monthly['amount'] - monthly['prev_month']) / monthly['prev_month']
实战:留存分析
留存率计算
# 活跃用户数据
activity = pd.DataFrame({
'user_id': [1, 1, 1, 2, 2, 3],
'date': pd.to_datetime(['2024-01-01', '2024-01-03', '2024-02-01',
'2024-01-01', '2024-01-15', '2024-01-01'])
})
# 1. 获取每个用户的首次活跃日期
first_active = activity.groupby('user_id')['date'].min().reset_index()
first_active.columns = ['user_id', 'first_date']
# 2. 合并后计算距首次活跃的天数
merged = activity.merge(first_active, on='user_id')
merged['days_since'] = (merged['date'] - merged['first_date']).dt.days
# 3. 按天数分桶统计留存用户数
retention = merged.groupby('days_since')['user_id'].nunique().reset_index()
retention.columns = ['days_since', 'retained_users']
# 4. 计算留存率
total_users = activity['user_id'].nunique()
retention['retention_rate'] = retention['retained_users'] / total_users
常见面试问题
Q1: resample 和 groupby 的区别?
答案:
resample专门针对时间序列索引的分组聚合,支持上下采样、插值groupby是通用的分组,需要先提取年月等字段- 实现同样效果:
df.resample('ME').sum()↔df.groupby(df.index.to_period('M')).sum() resample更简洁,且支持ffill/bfill等时序特有操作
Q2: rolling 和 expanding 的区别?
答案:
rolling(n):固定窗口 n,每次只看最近 n 个值expanding():累积窗口,从第 1 行算到当前行rolling(7).mean()= 7 日移动平均;expanding().sum()= 累积求和
Q3: 如何用 Pandas 计算同比环比?
答案:
- 环比:
df['col'].pct_change()→ 与上一期对比 - 同比:
df['col'].pct_change(periods=12)→ 与去年同期对比(月度数据) - 也可以用
shift()手动计算:(当期 - 上期) / 上期
Q4: pd.Timestamp 和 Python datetime 的区别?
答案:
pd.Timestamp是 Pandas 的时间标量,精度到纳秒,支持时区- Python
datetime.datetime精度到微秒 - Pandas 内部用
Timestamp,但两者可以互转:pd.Timestamp(dt_obj)和ts.to_pydatetime()