1. 时间序列数据初识

在数据分析工作中,有一类数据几乎无处不在:股票每日收盘价、气象站每小时温度、工厂设备每分钟振动幅值、网站每秒访问量。它们的共同点是——每个观测值都绑定在一个时间点上,且观测顺序本身携带信息。这就是时间序列数据。与普通表格数据不同,时间序列的行顺序不能随意打乱,因为“昨天”和“今天”的先后关系决定了趋势、周期与滞后效应的存在。

从定义上说,时间序列是按时间先后排列的、同一指标(或一组指标)的观测序列。它通常包含两个基本成分:时间索引与观测值。理解时间序列,第一步不是急着画图,而是先搞清楚两件事:时间轴是规则的还是不规则的,以及用 Pandas 的哪种数据结构来承载这条时间轴。

规则与不规则:两种时间轴

规则时间序列指相邻观测之间的时间间隔固定,例如日频(每个交易日或每个自然日一条)、小时频、分钟频。规则序列的好处是可以直接做位移、差分、重采样等操作,因为“上一条”与“下一条”的时间距离是确定的。不规则时间序列则指观测发生在不等间隔的时间点上,例如交易撮合记录、设备告警事件、用户下单行为——它们只在事件发生时才产生一条记录,间隔可能是几秒,也可能是几天。

需要特别提醒一个容易混淆的点:股票日线数据在“自然日”维度上是不规则的(周末与节假日没有交易),但在“交易日”维度上是规则的。因此判断规则性时,要先明确时间轴的口径。这个区分会直接影响后续重采样与缺失值识别的策略。

Pandas 的四个核心时间对象

Pandas 为时间序列提供了分工明确的基础类型,理解它们的职责边界是后续所有操作的前提。

  • Timestamp:表示单个时间点,精度可达纳秒,是标量。可以理解为“某一刻”。
  • DatetimeIndex:由一组 Timestamp 构成的索引,是时间序列最常用的“时间轴”。它让 DataFrame 具备按时间切片、按频率重采样、按时间对齐的能力。
  • Timedelta:表示两个时间点之间的时长(时间差),是标量;一组 Timedelta 构成 TimedeltaIndex。它回答的是“相隔多久”。
  • Period:表示一个时间区间(如“2024年3月”“2024年第2季度”),而非一个瞬间。它适合表达会计期间、月度汇总等“区间型”时间概念。

可以用一句话区分:Timestamp 是点,Timedelta 是两点之间的距离,Period 是一段区间,DatetimeIndex 是排好序的点序列。实际建模中,绝大多数时间序列以 DatetimeIndex 作为行索引,观测值放在列中。

Pandas 时间对象与时间轴结构 Timestamp 2024-03-01 2024-03-02 2024-03-03 2024-03-04 1 天 1 天 1 天 Timedelta:相邻时间点的间隔 Period:2024-03(整月区间,非某一瞬间) DatetimeIndex:上排四个 Timestamp 按时间排序后作为行索引 df.index = DatetimeIndex(['2024-03-01','2024-03-02','2024-03-03','2024-03-04']) → 支持按时间切片、重采样、频率对齐,是时间序列的“骨架”

加载真实数据并快速查看

下面以一份模拟的股票日线数据为例,走一遍从加载到初判的完整流程。假设文件 stock_daily.csv 包含 date, open, close, volume 四列,日期为字符串格式。示例数据为教学构造,非真实行情。

import pandas as pd

df = pd.read_csv("stock_daily.csv")
print(df.head())
print(df.dtypes)
print(df.shape)

此时 date 列仍是 object(字符串)类型,Pandas 不会自动把它当作时间。必须显式转换并设为索引,时间序列的能力才会被激活:

df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")
df = df.set_index("date").sort_index()

print(df.index)          # DatetimeIndex
print(df.index.freq)     # 若为 None,说明未声明频率
print(df.index.is_monotonic_increasing)  # 必须为 True

这里有两个关键检查点。其一,索引必须单调递增:若原始文件存在乱序或重复日期,切片与位移会得到错误结果,需用 sort_index() 排序,并用 df.index.duplicated().sum() 检查重复。其二,频率是否已声明:freq 为 None 并不代表数据不规则,只代表 Pandas 尚未确认。可用 pd.infer_freq(df.index) 推断,若返回 'B' 说明是工作日频率,返回 None 则提示间隔不一致,需要进一步核查。

接下来做初步的“体检”,快速判断数据质量与时间覆盖范围:

print(df.index.min(), df.index.max())   # 时间覆盖范围
print(df.index.to_series().diff().value_counts().head())  # 间隔分布
print(df.isna().sum())                  # 各列缺失情况
print(df.describe())                    # 数值分布概览

其中 diff().value_counts() 是判断规则性的实用手段:如果绝大多数间隔都是 1 天、少量为 3 天(周末),说明这是交易日规则序列;如果间隔值五花八门,则属于不规则序列,后续不应盲目使用固定频率的重采样。这一步的结论,直接决定后面缺失值识别时该用“按自然日补齐”还是“按事件间隔分析”。

加载阶段建议固定执行四件事:转 datetime、设索引、排序、查重复。这四步做完,时间序列的“地基”才算稳,后续的重采样、滑动窗口、缺失值识别才不会因索引问题产生隐蔽错误。

最后补充一个易错点:Period 与 DatetimeIndex 之间可以互相转换,但语义不同。df.index.to_period("M") 会把每个日期归入所属月份,适合做月度汇总;而直接对 DatetimeIndex 重采样则保留更细的时间信息。选择哪种,取决于你要回答的问题是“这个月总量多少”还是“这个月内每天如何变化”。