本节摘要:销售数据、日志、传感器读数都带着时间戳,而时间序列的处理套路和普通表格完全不同。本节掌握三件套:把日期列变成时间索引、用 resample 重采样改变时间粒度、用 rolling 做滑动窗口计算,最后学会提取周期特征(星期、月份)用于分析。
阅读完本节,你应当能够:
一张带日期的订单表,想回答"每周销售额怎么样",用 groupby 按周分组要先把日期格式化成周——麻烦还容易错。时间序列场景需要的是一整套"以时间为轴"的操作:把时间变成索引、按时间粒度重采样、算滑动平均看趋势。Pandas 为此提供了专门的时间序列工具,掌握后这类问题都是一行代码。
普通索引(数字、字符串)只负责定位;DatetimeIndex(时间索引)额外具备时间语义:可以按时间段切片、按周期重采样、按时间差偏移。pd.to_datetime 把日期字符串转成时间类型,set_index 把日期列设为索引,时间序列操作的舞台就搭好了。
两者都返回"以时间顺序计算"的结果,区别是 resample 改变粒度,rolling 保持粒度只做平滑。
import pandas as pd df = pd.DataFrame({ "日期": ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-04"], "销售额": [120, 150, 90, 180], }) df["日期"] = pd.to_datetime(df["日期"]) df = df.set_index("日期") print(df.index) # DatetimeIndex
💡 关键直觉:
set_index不修改原表,要df = df.set_index("日期")才会生效。设完时间索引,切片写df["2026-01-02":]这种"时间字符串切片"才可用。
# 按周汇总(W=周日为一周结束,MS=月初) weekly = df["销售额"].resample("W").sum() monthly = df["销售额"].resample("MS").sum() # 按天补全缺失日期 daily_full = df["销售额"].resample("D").sum()
⚠️ 常见坑:resample 要求索引是 DatetimeIndex,且默认把缺失的时间点当 0 或 NaN 处理。按周聚合时某周没数据,结果就是空的——看结果前先确认数据的实际覆盖范围。
# 7 日移动平均(前 7 天含当天) df["销售7日均"] = df["销售额"].rolling(7, min_periods=1).mean() # 3 日累计 df["3日累计"] = df["销售额"].rolling(3).sum()
min_periods=1 让数据不足窗口时也能出值(开头几天不用空着),画趋势线更平滑。
df["星期"] = df.index.dayofweek # 0=周一 ... 6=周日 df["月份"] = df.index.month df["季度"] = df.index.quarter df["是否周末"] = df.index.dayofweek >= 5
提取"星期几""是否周末"这类周期特征,是分析"周末销售是否更高"这类问题的前提,也是机器学习建模里的常见特征工程。
df = pd.DataFrame({ "日期": pd.date_range("2026-01-01", periods=30, freq="D"), "销售额": [100 + i * 3 + (i % 7) * 10 for i in range(30)], }).set_index("日期") # 1. 周汇总 weekly = df["销售额"].resample("W").sum() # 2. 移动平均看趋势 df["14日均"] = df["销售额"].rolling(14, min_periods=1).mean() # 3. 周末 vs 工作日对比 df["是否周末"] = df.index.dayofweek >= 5 compare = df.groupby("是否周末")["销售额"].mean() print(weekly) print(compare)
三步走完:周报有了、趋势线有了、"周末效应"是否存在的证据也有了。
| 需求 | 代码 |
|---|---|
| 按天 | resample("D") |
| 按周 | resample("W") |
| 按月 | resample("MS") |
| 按季度 | resample("QS") |
| 移动平均 | rolling(7).mean() |
| 累计和 | cumsum() |
| 时间差 | df.index.to_series().diff() |
时间索引最爽的用法是按时间范围直接切片:
df = df.set_index("日期") df["2026-01-01"] # 某一天 df["2026-01-01":"2026-01-07"] # 某一段(含头含尾) df["2026-01"] # 整个一月 df.loc["2026-01-02":] # 某天之后
⚠️ 常见坑:时间切片要求索引是 DatetimeIndex,且字符串格式与索引一致。切片是"含头含尾",与普通切片的"含头不含尾"不同——写时间范围时差一天,结果就多或少一天。
# 相邻两天的时间差 df["天数间隔"] = df.index.to_series().diff().dt.days # 滞后特征:昨天的销售额(预测类任务常用) df["昨日销售"] = df["销售额"].shift(1) # 同比增长(月度数据) monthly["同比"] = monthly["销售额"].pct_change(12) * 100
shift 造滞后特征、pct_change 算变化率,是时间序列分析与预测的基础构件。理解"滞后一天"的概念,是理解时间序列建模的起点。
| 业务问题 | 对应操作 |
|---|---|
| 周趋势如何 | 按周 resample + 折线图 |
| 周末效应是否存在 | dayofweek 分组对比 |
| 趋势还是噪声 | rolling 移动平均平滑 |
| 月度增速 | pct_change 月度对比 |
| 缺数据怎么补 | ffill / interpolate |
时间序列分析的核心始终是"把时间维度变成可比较的度量"——粒度对齐、趋势提取、周期对比,三者覆盖了绝大多数需求。
| 坑 | 现象 | 解法 |
|---|---|---|
| 索引不是时间类型 | resample 报错 | pd.to_datetime 后 set_index |
| 时间切片差一天 | 忘了含头含尾 | 确认切片边界是否含终点 |
| resample 结果有空 | 数据缺时间点 | 先看覆盖范围,再考虑补全 |
| rolling 开头是 NaN | 窗口不足 | 加 min_periods=1 |
| 时区混乱 | 时间对不上 | 统一 tz_localize / tz_convert |
⚠️ 常见坑:跨时区或跨夏令时的数据,直接 to_datetime 可能解析错。生产环境里时间字段务必统一时区再入库,这是时序分析里最隐蔽也最费时间的坑。
时间序列的最终目的常常是预测。做预测前,先把历史数据整理成"模型能吃的形状":
# 日粒度 → 周粒度(稳定波动) weekly = df["销售额"].resample("W").sum() # 构造滞后特征供模型使用 features = pd.DataFrame({ "上周销售": weekly.shift(1), "上上周销售": weekly.shift(2), "目标": weekly, }).dropna() print(features.head())
"上周、上上周的销售 → 预测本周"是最朴素的时序特征构造,也是理解更复杂预测模型的起点。把这一步跑通,时间序列章节就算真正入门了。
min_periods=1 让窗口开头的值不空,画趋势更顺数据处理的能力已经全面,下一节把它打包成工程——可复用的处理管道与工程化习惯。