时间序列数据无处不在:股票、销量、日志、传感器……pandas 对时间序列的支持极其强大:日期解析、日期索引、重采样、滚动窗口、时间偏移。本篇文章系统讲解 pandas 的时间序列武器库。
目录
1. 日期与时间的表示
2. 生成日期范围:date_range
3. 把列设为日期索引
4. 日期索引的切片与选择
5. 重采样:resample
6. 滚动窗口:rolling
7. 时间偏移:shift / diff / 日期运算
8. dt 访问器:提取日期成分
9. 时区处理
10. 时间序列实战案例
11. 常见坑与注意事项
12. 本章小结与练习
1. 日期与时间的表示
pandas 中时间类型有三种:
| 类型 | 说明 | 例子 |
|------|------|------|
| Timestamp | 一个时间点 | 2025-01-01 08:30:00 |
| DatetimeIndex | 时间索引(多个时间点) | 日期列或索引 |
| Timedelta | 时间差 | 2 days 03:00:00 |
import pandas as pd
# 单个时间点
ts = pd.Timestamp("2025-01-01 08:30:00")
print(ts) # 2025-01-01 08:30:00
print(ts.year, ts.month, ts.day) # 2025 1 1
# 从字符串解析(会自动识别多种格式)
print(pd.to_datetime("2025-01-01"))
print(pd.to_datetime("20250101"))
print(pd.to_datetime("2025/1/1"))
print(pd.to_datetime("2025年1月1日"))
print(pd.to_datetime("2025-01-01 08:30:00"))
# 当前时间
print(pd.Timestamp.now())
# 时间差
print(pd.to_timedelta("2 days 3 hours"))
2. 生成日期范围:date_range
# 按天数生成(默认频率为 D)
dates = pd.date_range("2025-01-01", periods=5)
print(dates)
# DatetimeIndex(['2025-01-01', ..., '2025-01-05'], dtype='datetime64[ns]', freq='D')
# 指定起止
pd.date_range("2025-01-01", "2025-01-10") # 10 天
# 指定频率
pd.date_range("2025-01-01", periods=3, freq="W") # 每周
pd.date_range("2025-01-01", periods=3, freq="M") # 每月末
pd.date_range("2025-01-01", periods=3, freq="MS") # 每月初
pd.date_range("2025-01-01", periods=4, freq="h") # 每小时
pd.date_range("2025-01-01", periods=4, freq="30min") # 每 30 分钟
pd.date_range("2025-01-01", periods=3, freq="Q") # 每季度末
pd.date_range("2025-01-01", periods=3, freq="Y") # 每年末
频率代码速查
| 代码 | 含义 | 代码 | 含义 |
|------|------|------|------|
| D | 天 | W | 周 |
| h | 小时 | M | 月末 |
| min | 分钟 | MS | 月初 |
| s | 秒 | Q | 季度末 |
| B | 工作日 | Y | 年末 |
| W-MON | 每周一 | 2D | 每 2 天 |
# 工作日
pd.date_range("2025-01-01", periods=5, freq="B")
# 组合频率
pd.date_range("2025-01-01", periods=4, freq="2D") # 每 2 天
pd.date_range("2025-01-01", periods=4, freq="W-MON") # 每周一
3. 把列设为日期索引
import numpy as np
df = pd.DataFrame({
"日期": pd.date_range("2025-01-01", periods=10, freq="D"),
"销量": np.random.randint(50, 200, 10),
})
print(df)
# 转日期类型(若还是字符串)
df["日期"] = pd.to_datetime(df["日期"])
# 设为索引
df = df.set_index("日期")
print(df)
print(df.index) # DatetimeIndex
销量
日期
2025-01-01 154
2025-01-02 183
2025-01-03 137
...
4. 日期索引的切片与选择
# 按日期标签取(字符串即可,自动转换)
print(df.loc["2025-01-03"])
print(df.loc["2025-01-01":"2025-01-05"]) # 闭区间,包含两端
# 按年份 / 月份部分匹配
print(df.loc["2025-01"]) # 整个 1 月
print(df.loc["2025"]) # 整个 2025 年
# 布尔筛选(列是日期时)
df2 = df.reset_index()
print(df2[df2["日期"] >= "2025-01-05"])
print(df2[(df2["日期"] >= "2025-01-02") & (df2["日期"] <= "2025-01-06")])
# between_time / at_time(对时间成分)
df_hour = df.resample("h").ffill() # 示例:按小时
日期索引切片包含终点,且支持"2025-01"这样的部分匹配,这是其他类型索引没有的特性。
5. 重采样:resample
resample 把时间序列按新频率聚合(降采样)或填充(升采样),是时间序列最常用的操作。
5.1 降采样(高频 → 低频)
# 按天 -> 按周求和
weekly = df.resample("W").sum()
print(weekly)
# 按天 -> 按月求均值
monthly = df.resample("M").mean()
print(monthly)
# 按天 -> 按季度
quarterly = df.resample("Q").sum()
| resample 频率 | 说明 |
|---------------|------|
| W | 按周 |
| M | 按月末 |
| MS | 按月初 |
| Q | 按季度末 |
| Y | 按年末 |
| h | 按小时 |
5.2 升采样(低频 → 高频)
# 按月 -> 按天,前向填充
df_daily = monthly.resample("D").ffill()
# 用插值填充
df_daily2 = monthly.resample("D").interpolate()
# 先按天再按小时
df_hourly = df.resample("h").ffill()
5.3 resample 的其他用法
# 多列多聚合
result = df.resample("W").agg({"销量": ["sum", "mean", "max"]})
# 自定义函数
df.resample("W").apply(lambda x: x["销量"].std())
# 从特定锚点开始
df.resample("W-MON").sum() # 以周一开始的周
# 每个周期内的观测次数
df.resample("W").count()
5.4 resample vs groupby
# groupby + 按时间分组(等价做法)
df.groupby(pd.Grouper(freq="W")).sum()
记忆:resample(freq)几乎等价于groupby(pd.Grouper(freq=...)),只是专为时间设计。
6. 滚动窗口:rolling
rolling 计算滑动窗口内的统计量,常用于均线、平滑、波动率等。
# 3 天滚动均值
df["销量_3日均线"] = df["销量"].rolling(3).mean()
print(df)
销量 销量_3日均线
日期
2025-01-01 154 NaN
2025-01-02 183 NaN
2025-01-03 137 158.000000
2025-01-04 157 159.000000
...
# 滚动求和 / 最大值 / 标准差
df["销量_7日累计"] = df["销量"].rolling(7).sum()
df["销量_5日最大"] = df["销量"].rolling(5).max()
df["波动率"] = df["销量"].rolling(5).std()
# 居中窗口
df["销量_中心均值"] = df["销量"].rolling(5, center=True).mean()
# 最小观测数
df["销量_均线2"] = df["销量"].rolling(3, min_periods=1).mean() # 前 2 天也算
# 基于时间窗口(不固定条数,固定天数)
df_t = df.resample("D").ffill()
df_t["3日滚动"] = df_t["销量"].rolling("3D").sum()
rolling 的常见组合
# 滚动排名
df["滚动排名"] = df["销量"].rolling(5).rank()
# 滚动分位数
df["滚动分位"] = df["销量"].rolling(5).quantile(0.8)
# 滚动 apply 自定义
df["滚动极差"] = df["销量"].rolling(5).apply(lambda x: x.max() - x.min())
# 指数加权移动平均(EWMA):对近期数据加权更高
df["EWMA"] = df["销量"].ewm(span=5).mean()
什么时候用rollingvsewm:rolling等权窗口,ewm指数加权(近期权重高),后者更贴近"最新信息更重要"的业务直觉。
7. 时间偏移:shift / diff / 日期运算
7.1 shift:上下平移(滞后/领先)
# 前一天的值(滞后 1 期)
df["昨日销量"] = df["销量"].shift(1)
# 后一天的值(领先 1 期)
df["明日销量"] = df["销量"].shift(-1)
# 多期
df["上周同日"] = df["销量"].shift(7)
# 对比昨日涨跌
df["环比变化"] = df["销量"] - df["销量"].shift(1)
df["环比变化率"] = df["销量"].pct_change() # 变化率
销量 昨日销量 环比变化 环比变化率
2025-01-01 154 NaN NaN NaN
2025-01-02 183 154.0 29.0 0.188312
2025-01-03 137 183.0 -46.0 -0.251366
7.2 diff:差分
# 一阶差分(等价 本期-上期)
df["一阶差分"] = df["销量"].diff()
# 二阶差分
df["二阶差分"] = df["销量"].diff(2)
# 百分比差分
df["百分比差分"] = df["销量"].diff() / df["销量"].shift(1)
7.3 日期之间的运算
# 两列日期相减 -> Timedelta
df["订单日期"] = pd.to_datetime("2025-01-05")
df["发货日期"] = pd.to_datetime("2025-01-08")
df["间隔天数"] = (df["发货日期"] - df["订单日期"]).dt.days
print(df["间隔天数"].head())
# 0 3
# 1 3
# 2 3
# 加/减时间偏移
print(pd.Timestamp("2025-01-01") + pd.Timedelta(days=10))
print(pd.Timestamp("2025-01-01") + pd.offsets.MonthEnd(1))
# 判断是否在范围内
print(pd.Timestamp("2025-03-15") < pd.Timestamp("2025-06-01"))
8. dt 访问器:提取日期成分
当日期是普通列(不是索引)时,用 .dt 访问器提取成分:
df2 = df.reset_index()
df2["年"] = df2["日期"].dt.year
df2["月"] = df2["日期"].dt.month
df2["日"] = df2["日期"].dt.day
df2["星期"] = df2["日期"].dt.dayofweek # 0=周一 ... 6=周日
df2["星期名"] = df2["日期"].dt.day_name() # Monday...
df2["季度"] = df2["日期"].dt.quarter
df2["周几_中文"] = df2["日期"].dt.strftime("%A")
df2["是否周末"] = df2["日期"].dt.dayofweek >= 5
df2["小时"] = df2["日期"].dt.hour # 有时间的列才有意义
df2["一年中第几天"] = df2["日期"].dt.dayofyear
df2["ISO周"] = df2["日期"].dt.isocalendar().week
常用日期格式化(strftime):
| 代码 | 含义 | 代码 | 含义 |
|------|------|------|------|
| %Y | 4 位年 | %m | 2 位月 |
| %d | 2 位日 | %H | 24 小时制 |
| %M | 分钟 | %S | 秒 |
| %A | 星期英文 | %j | 年中第几天 |
df2["日期字符串"] = df2["日期"].dt.strftime("%Y-%m-%d")
9. 时区处理
# 指定时区
ts = pd.Timestamp("2025-01-01 08:00:00", tz="Asia/Shanghai")
print(ts)
# 2025-01-01 08:00:00+08:00
# 转换为其他时区
print(ts.tz_convert("UTC"))
# 2025-01-01 00:00:00+00:00
# 无时区 -> 有时区
ts2 = pd.Timestamp("2025-01-01 08:00:00")
print(ts2.tz_localize("Asia/Shanghai"))
# 日期范围带时区
pd.date_range("2025-01-01", periods=3, tz="UTC")
10. 时间序列实战案例
import pandas as pd
import numpy as np
# 生成 90 天日销售数据
np.random.seed(42)
sales = pd.DataFrame({
"日期": pd.date_range("2025-01-01", periods=90, freq="D"),
"销量": np.random.randint(80, 300, 90).astype(float),
})
sales.loc[sales["日期"].dt.dayofweek >= 5, "销量"] *= 1.5 # 周末销量高
sales = sales.set_index("日期")
# 1. 周度汇总
weekly = sales.resample("W").sum()
print(weekly.head())
# 2. 月度汇总 + 描述
monthly = sales.resample("M").agg({"销量": ["sum", "mean", "max"]})
print(monthly)
# 3. 7 日滚动均线
sales["7日均线"] = sales["销量"].rolling(7).mean()
# 4. 环比变化率
sales["日环比"] = sales["销量"].pct_change()
# 5. 周末效应对比
sales["周末"] = sales.index.dayofweek >= 5
print(sales.groupby("周末")["销量"].mean())
# 6. 周几效应
sales["周几"] = sales.index.day_name()
print(sales.groupby("周几")["销量"].mean().sort_values(ascending=False))
# 7. 找销量最高的前 10 天
print(sales["销量"].nlargest(10))
# 8. 缺失日期补全(重采样后填充)
full = sales.resample("D").asfreq() # 生成完整日期(缺的变 NaN)
full = full.ffill() # 前向填充
11. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| 日期是 object 类型 | 无法切片/resample | pd.to_datetime 转换 |
| 时间格式不统一 | 解析失败 | format= 指定 |
| resample 后数据变少 | 降采样聚合了 | 这是正常行为,注意聚合函数 |
| 升采样出现 NaN | 新频率无数据 | ffill / interpolate |
| shift 边界 NaN | 首行/末行 NaN | fillna 或业务上忽略 |
| 日期索引切片 | 忘记闭区间特性 | 用 ":" 包含端点 |
| 用字符串比较日期列 | 类型错误 | 先 to_datetime 再比较 |
| 时间戳 vs 日期混淆 | 时间被忽略 | dt.normalize() 去时间部分 |
12. 本章小结与练习
小结
- 类型:
Timestamp/DatetimeIndex/Timedelta; - 生成:
date_range(start, periods, freq),freq 控制频率; - 索引:日期索引支持字符串切片、年份/月份部分匹配;
- 重采样:
resample(freq).agg(...)降采样聚合、升采样填充; - 滚动:
rolling(n).mean()/sum()/std(),ewm指数加权; - 平移:
shift(n)、diff()、pct_change(); - 提取:
.dt.year/month/day/dayofweek/quarter。
练习题
1. 用 date_range 生成 2025 年所有周一,共多少天?
2. 对 90 天日数据按周求和、按月求均值。
3. 计算 7 日滚动均线并对比原序列。
4. 用 shift 计算每日销量环比变化率。
5. 提取日期的年、月、周几,统计每周平均销量。
6. 将低频月数据重采样到日并前向填充。
下一篇预告:第 15 章 数据可视化 —— 用 pandas 内置绘图接口快速出图。
文章回复
0 条公开回复