RSS
菜单
全部文章快讯开发科技深度热点

第 14 章 时间序列分析

内容摘要

时间序列数据无处不在:股票、销量、日志、传感器……pandas 对时间序列的支持极其强大:**日期解析、日期索引、重采样、滚动窗口、时间偏移**。本篇文章系统讲解 pandas 的时间序列武器库。

时间序列数据无处不在:股票、销量、日志、传感器……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()
什么时候用 rolling vs ewm: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 内置绘图接口快速出图。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 14 章 时间序列分析》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。