3.3.9 时间序列
- 掌握日期时间类型的创建和转换
- 学会时间序列的索引与切片
- 掌握重采样(resample)和频率转换
- 学会滚动窗口计算(rolling)
先建立一张地图
Section titled “先建立一张地图”时间序列更适合按“先把日期变成可操作对象,再按时间维度做分析”来理解:

所以这节真正想解决的是:
- 日期为什么不能只当普通字符串用
- 为什么时间数据一旦整理好,后面的分析会完全不一样
为什么需要时间序列?
Section titled “为什么需要时间序列?”很多数据都和时间相关——股票价格、销售数据、网站访问量、服务日志、模型指标……处理时间数据是数据分析的必备技能。
一个更适合新人的总类比
Section titled “一个更适合新人的总类比”你可以把时间序列理解成:
- 给数据加上一条真正有顺序的时间轴
有了这条轴以后,你不只是能问:
- 现在是多少
还会开始问:
- 比上个月高还是低
- 最近 7 天是不是在上升
- 去年同月和今年同月差多少
日期时间类型
Section titled “日期时间类型”import pandas as pdimport numpy as np
# 创建单个时间戳ts = pd.Timestamp("2024-01-15")print(ts) # 2024-01-15 00:00:00print(ts.year) # 2024print(ts.month) # 1print(ts.day) # 15print(ts.day_name()) # Monday
# 更多格式ts2 = pd.Timestamp("2024-01-15 14:30:00")ts3 = pd.Timestamp(year=2024, month=3, day=20)字符串转日期
Section titled “字符串转日期”# 单列转换dates = pd.Series(["2024-01-15", "2024-02-20", "2024-03-10"])dt_series = pd.to_datetime(dates)print(dt_series)print(dt_series.dtype) # datetime64[ns]
# 处理不同格式pd.to_datetime("15/01/2024", format="%d/%m/%Y")pd.to_datetime("2024年3月15日", format="%Y年%m月%d日")
# 处理无法解析的值dirty = pd.Series(["2024-01-15", "not a date", "2024-03-10"])clean = pd.to_datetime(dirty, errors="coerce") # 无法解析的变成 NaTprint(clean)# 0 2024-01-15# 1 NaT ← Not a Time# 2 2024-03-10# 创建日期范围dates = pd.date_range("2024-01-01", periods=10, freq="D") # 每天print(dates)
# 不同频率pd.date_range("2024-01-01", periods=12, freq="ME") # 每月末pd.date_range("2024-01-01", periods=4, freq="QE") # 每季度末pd.date_range("2024-01-01", "2024-12-31", freq="W") # 每周
# 常用频率代码# D=天, W=周, ME=月末, MS=月初, QE=季末, YE=年末# h=小时, min=分钟, s=秒# B=工作日第一次处理日期列时,最该先记什么?
Section titled “第一次处理日期列时,最该先记什么?”最值得先记的是:
日期列最好先转成真正的 datetime,再谈后面所有时间分析。
如果还停留在字符串层, 很多事情都很难自然地做:
- 切月份
- 算时间差
- 做重采样
时间序列数据
Section titled “时间序列数据”创建时间序列 DataFrame
Section titled “创建时间序列 DataFrame”# 模拟 2024 年每天的销售数据rng = np.random.default_rng(seed=42)dates = pd.date_range("2024-01-01", periods=365, freq="D")sales = pd.DataFrame({ "日期": dates, "销售额": rng.integers(5000, 20000, 365) + \ np.sin(np.arange(365) * 2 * np.pi / 365) * 3000 # 加入季节性})sales = sales.set_index("日期")print(sales.head())print(sales.shape) # (365, 1)提取日期组件
Section titled “提取日期组件”df = pd.DataFrame({ "日期": pd.date_range("2024-01-01", periods=100, freq="D"), "销量": rng.integers(10, 100, 100)})
# 用 dt 访问器提取日期组件df["年"] = df["日期"].dt.yeardf["月"] = df["日期"].dt.monthdf["日"] = df["日期"].dt.daydf["星期几"] = df["日期"].dt.day_name()df["是否周末"] = df["日期"].dt.dayofweek >= 5 # 5=周六, 6=周日df["第几周"] = df["日期"].dt.isocalendar().week
print(df.head())时间索引切片
Section titled “时间索引切片”当日期是索引时,可以用字符串方便地切片:
# sales 的索引是日期# 选取 2024 年 3 月的数据print(sales.loc["2024-03"])
# 选取 2024 年第一季度print(sales.loc["2024-01":"2024-03"])
# 选取某一天print(sales.loc["2024-06-15"])一个很适合初学者先记的时间分析顺序
Section titled “一个很适合初学者先记的时间分析顺序”更稳的顺序通常是:
- 先转成 datetime
- 先提取年 / 月 / 星期几
- 再把日期设成索引
- 最后再做重采样和滚动窗口
这个顺序特别重要,因为很多新人是跳着学,最后把时间索引和普通列混在一起。
重采样(resample)
Section titled “重采样(resample)”
重采样是时间序列最核心的操作——改变数据的时间频率。
降采样(高频 → 低频)
Section titled “降采样(高频 → 低频)”# 每日数据 → 每月数据monthly = sales.resample("ME").sum() # 月末汇总print(monthly.head())
# 每日 → 每周weekly = sales.resample("W").mean() # 周平均
# 每日 → 每季度quarterly = sales.resample("QE").agg({ "销售额": ["sum", "mean", "max"]})print(quarterly)升采样(低频 → 高频)
Section titled “升采样(低频 → 高频)”# 月度数据 → 每日数据(需要填充)daily = monthly.resample("D").ffill() # 前向填充# 或daily = monthly.resample("D").interpolate() # 插值一个很适合初学者先记的判断表
Section titled “一个很适合初学者先记的判断表”| 你想做什么 | 更稳的第一反应 |
|---|---|
| 每日变每月 | resample() 降采样 |
| 每月变每日 | resample() 升采样 |
| 看最近 7 天平均 | rolling() |
| 看从开始到现在平均 | expanding() |
这个表特别适合新人,因为它会把时间序列常见操作重新压回几种最常见的问题。
滚动窗口(rolling)
Section titled “滚动窗口(rolling)”滚动窗口对连续的 N 个数据点计算统计量——常用于平滑数据和计算移动平均。
# 7 日移动平均(平滑日常波动)sales["MA7"] = sales["销售额"].rolling(window=7).mean()
# 30 日移动平均(看长期趋势)sales["MA30"] = sales["销售额"].rolling(window=30).mean()
print(sales.head(10))# 前 6 天的 MA7 是 NaN(不够 7 天计算)其他滚动统计
Section titled “其他滚动统计”# 滚动标准差(波动性)sales["STD7"] = sales["销售额"].rolling(7).std()
# 滚动最大值sales["MAX7"] = sales["销售额"].rolling(7).max()
# 滚动求和sales["SUM7"] = sales["销售额"].rolling(7).sum()expanding:累积计算
Section titled “expanding:累积计算”# 累积均值(从头到当前的均值)sales["累积均值"] = sales["销售额"].expanding().mean()
# 累积最大值sales["历史最高"] = sales["销售额"].expanding().max()为什么 rolling 这么常见?
Section titled “为什么 rolling 这么常见?”因为真实时间数据通常很抖。 如果你只盯每天的原始值,很容易被波动带偏。
rolling 最值得先记住的价值就是:
- 帮你从抖动里看趋势
df = pd.DataFrame({ "注册时间": pd.to_datetime(["2023-01-15", "2023-06-20", "2024-01-10"]), "最后登录": pd.to_datetime(["2024-06-01", "2024-05-15", "2024-06-10"])})
# 计算时间差df["使用天数"] = (df["最后登录"] - df["注册时间"]).dt.daysprint(df)
# 距今天数df["注册至今天数"] = (pd.Timestamp.now() - df["注册时间"]).dt.days实战:销售趋势分析
Section titled “实战:销售趋势分析”import pandas as pdimport numpy as np
rng = np.random.default_rng(seed=42)
# 创建 2 年的日销售数据dates = pd.date_range("2023-01-01", "2024-12-31", freq="D")n = len(dates)
sales = pd.DataFrame({ "日期": dates, "销售额": ( 10000 + # 基础值 np.sin(np.arange(n) * 2 * np.pi / 365) * 3000 + # 季节性 np.arange(n) * 5 + # 增长趋势 rng.normal(0, 1000, n) # 随机波动 ).astype(int)}).set_index("日期")
# 1. 月度汇总monthly = sales.resample("ME").agg( 月销售额=("销售额", "sum"), 日均销售额=("销售额", "mean"), 最高日销售额=("销售额", "max"))print("=== 月度汇总 ===")print(monthly.head())
# 2. 移动平均看趋势sales["MA30"] = sales["销售额"].rolling(30).mean()print("\n=== 30日移动平均(最后5天)===")print(sales[["销售额", "MA30"]].tail())
# 3. 同比增长(和去年同月比)monthly_pivot = sales.resample("ME")["销售额"].sum()monthly_pivot.index = monthly_pivot.index.to_period("M")# 简单计算 2024 年各月 vs 2023 年同月m2024 = monthly_pivot["2024"]m2023 = monthly_pivot["2023"]
print("\n=== 2024 vs 2023 月度对比 ===")for m24, m23 in zip(m2024.items(), m2023.items()): month = m24[0].month growth = (m24[1] - m23[1]) / m23[1] * 100 print(f" {month}月: 2023={m23[1]:,.0f}, 2024={m24[1]:,.0f}, 增长率={growth:+.1f}%")
# 4. 每周几的销售差异sales_with_dow = sales.copy()sales_with_dow["星期几"] = sales_with_dow.index.day_name()dow_avg = sales_with_dow.groupby("星期几")["销售额"].mean()print("\n=== 各星期几的平均销售额 ===")print(dow_avg.sort_values(ascending=False))这个小实战最值得先学到什么?
Section titled “这个小实战最值得先学到什么?”最值得先学到的不是某个函数名, 而是时间分析通常会先从这几步开始:
- 先汇总
- 再看趋势
- 再看同比 / 环比
- 最后再看周期差异
这会比一上来就直接做复杂预测更稳很多。
学完这一页,至少保留这张证据卡:
- 数据框状态
- 列、数据类型、行数、缺失值和样本行
- 操作
- 读/写、select/filter、清洗、转换、groupby、merge,或时间序列步骤
- 输出
- 结果表、保存的文件、聚合、连接结果,或时间索引视图
- 失败检查
- dtype 不匹配、缺失数据、重复键、链式赋值或时间频率错误
- 期望产出
- 前后对比表格样本,以及转换原因
| 操作 | 方法 | 用途 |
|---|---|---|
| 字符串转日期 | pd.to_datetime() | 类型转换 |
| 日期范围 | pd.date_range() | 生成连续日期 |
| 提取组件 | .dt.year/month/day | 拆解日期 |
| 重采样 | .resample() | 改变时间频率 |
| 滚动窗口 | .rolling() | 移动平均、平滑 |
| 累积计算 | .expanding() | 累积统计 |
| 时间差 | 相减 .dt.days | 计算间隔 |
这节最该带走什么
Section titled “这节最该带走什么”- 时间序列不只是“多一列日期”,而是分析方式变了
- 先把日期转成 datetime,再谈切片、重采样和滚动窗口
resample负责改时间频率,rolling负责看局部趋势
章节总结:Pandas 知识全景
Section titled “章节总结:Pandas 知识全景”恭喜你完成了 Pandas 的全部内容!来回顾一下:
flowchart TB subgraph "第2章 Pandas 数据处理" A["2.1 核心数据结构<br/>Series + DataFrame"] --> B["2.2 数据读写<br/>CSV/Excel/JSON"] B --> C["2.3 选择与过滤<br/>loc/iloc/布尔索引"] C --> D["2.4 数据清洗<br/>缺失值/重复值/异常值"] D --> E["2.5 数据转换<br/>apply/map/排序/分箱"] E --> F["2.6 分组与聚合<br/>groupby/agg/pivot_table"] F --> G["2.7 数据合并<br/>merge/concat"] G --> H["2.8 时间序列<br/>resample/rolling"] end
H --> I["✅ Pandas 掌握!<br/>准备进入数据可视化 →"]
style I fill:#4caf50,color:#fff✅ 自检: 给你一份销售数据 CSV,你能用 Pandas 清洗缺失值、按月份和产品分组统计销售额、并找出每月销售最高的产品吗?回想一下第 1 章的预热练习——现在是不是简洁多了?
练习 1:日期处理
Section titled “练习 1:日期处理”# 创建一个包含"2024-01-01"到"2024-12-31"的日期 DataFrame# 1. 提取月份、星期几# 2. 标记是否为工作日# 3. 计算每月的工作日天数练习 2:时间序列分析
Section titled “练习 2:时间序列分析”# 用上面的 sales 数据# 1. 计算 7 日和 30 日移动平均# 2. 找出销售额最高和最低的月份# 3. 计算每月的环比增长率(本月vs上月)# 4. 分析周末vs工作日的销售差异练习 3:综合实战
Section titled “练习 3:综合实战”# 模拟一个 App 的用户活跃数据(365天)# 包含:日期、DAU(日活跃用户)、新增用户、收入# 1. 计算周活跃用户数(WAU)和月活跃用户数(MAU)# 2. 计算 7 日留存率趋势# 3. 用 rolling 计算 ARPU(每用户平均收入)的 30 日平均# 4. 找出用户增长最快的月份参考实现与讲解
- 先用
pd.to_datetime把日期字符串转成时间类型,再创建月份、星期、季度、是否工作日等日历特征。 - 日历聚合用
resample,移动窗口用rolling,增长率用pct_change。这三个方法回答的是不同类型的问题。 - 产品或用户指标按时间分析时,必须写清时间粒度。日、周、月汇总即使用同一批原始事件,也可能讲出不同故事。