跳到主要内容

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()

相关链接