Chapter 09

时间序列分析

DatetimeIndex、resample 重采样、滚动计算、时区处理——掌握时序数据的完整工具链

时间序列数据的重要性

时间序列数据是现代数据分析中最常见的数据类型之一:股票价格、服务器监控指标、电商销售数据、用户行为日志……几乎所有业务系统都在实时产生时间序列数据。Pandas 提供了业界最完整的时间序列处理工具箱,这也是它在金融分析领域广受欢迎的重要原因。

时间相关的核心类型

创建时间序列数据

import pandas as pd
import numpy as np

# ── pd.to_datetime:将字符串/整数转为 Timestamp ──
pd.to_datetime('2024-01-15')           # Timestamp('2024-01-15 00:00:00')
pd.to_datetime('15/01/2024', dayfirst=True)  # 日在前
pd.to_datetime('20240115', format='%Y%m%d')  # 指定格式
pd.to_datetime(1705276800, unit='s')          # Unix 时间戳(秒)

# 转换整列
df['date'] = pd.to_datetime(df['date_str'], errors='coerce')

# ── pd.date_range:生成等间隔的日期序列 ──
pd.date_range('2024-01-01', '2024-12-31', freq='D')    # 每天
pd.date_range('2024-01-01', periods=12, freq='ME')      # 每月末,12期
pd.date_range('2024-01-01', periods=4, freq='QE')       # 每季末,4期
pd.date_range('2024-01-01', periods=52, freq='W-MON')   # 每周一
pd.date_range('2024-01-01', periods=100, freq='B')      # 工作日
pd.date_range('2024-01-01', periods=48, freq='h')       # 每小时

# ── 创建时间序列 DataFrame ──
dates = pd.date_range('2024-01-01', periods=365, freq='D')
ts = pd.Series(
    np.random.randn(365).cumsum() + 100,  # 随机游走
    index=dates,
    name='price',
)

Pandas 2.x 的频率别名变更

⚠️
重要变更:Pandas 2.2 的频率别名重命名 为了统一语义,Pandas 2.2 对部分频率别名进行了重命名,旧别名在 2.2 中触发 FutureWarning,并将在 3.0 中移除:
  • 'M''ME'(Month End 月末)
  • 'Q''QE'(Quarter End 季末)
  • 'Y' / 'A''YE'(Year End 年末)
  • 'H''h'(小时,改为小写)
  • 'T''min'(分钟)
  • 'S''s'(秒,改为小写)
  • 'MS''MS'(月初,保持不变)

DatetimeIndex 的切片与选择

有了 DatetimeIndex,可以用字符串直接进行日期切片,极为便捷:

import pandas as pd
import numpy as np

dates = pd.date_range('2023-01-01', '2024-12-31', freq='D')
ts = pd.Series(np.random.randn(len(dates)), index=dates)

# 按年选择
ts['2023']             # 2023年的所有数据
ts['2024']             # 2024年的所有数据

# 按月选择
ts['2023-06']          # 2023年6月
ts['2023-Q1']          # 2023年第一季度

# 按日期范围切片(loc 闭区间)
ts.loc['2023-06-01':'2023-08-31']  # 夏季数据(含两端)

# .dt 访问器——提取时间各组成部分
df['year']    = df['date'].dt.year
df['month']   = df['date'].dt.month
df['day']     = df['date'].dt.day
df['hour']    = df['date'].dt.hour
df['weekday'] = df['date'].dt.dayofweek  # 0=周一,6=周日
df['week_name']= df['date'].dt.day_name()  # 'Monday', 'Tuesday'...
df['quarter'] = df['date'].dt.quarter
df['is_weekend'] = df['date'].dt.dayofweek >= 5
df['day_of_year'] = df['date'].dt.dayofyear

# 格式化为字符串
df['month_str'] = df['date'].dt.strftime('%Y年%m月')

resample:时间序列的重采样

重采样(Resampling)是时间序列中最核心的操作之一,分为两种:

import pandas as pd
import numpy as np

# 日频数据
daily = pd.Series(
    np.random.randn(365).cumsum() + 100,
    index=pd.date_range('2024-01-01', periods=365, freq='D'),
)

# ── 降采样 ──
monthly_mean  = daily.resample('ME').mean()   # 月均值(月末频)
monthly_sum   = daily.resample('ME').sum()    # 月总和
monthly_last  = daily.resample('ME').last()   # 月末值
monthly_first = daily.resample('MS').first()  # 月初值(月初频 Month Start)
quarterly     = daily.resample('QE').agg({'mean', 'max', 'min'})
weekly_ohlc   = daily.resample('W').ohlc()   # 周K线(开高低收)

# ── 升采样 ──
# 月频 → 日频
monthly = pd.Series(
    [100, 110, 105],
    index=pd.date_range('2024-01-31', periods=3, freq='ME')
)
daily_up = monthly.resample('D').ffill()    # 前向填充(保持月末值到下月)
daily_up = monthly.resample('D').interpolate(method='linear')  # 线性插值

# ── resample + agg(Named Aggregation)──
# 对 DataFrame 的多列进行不同聚合
df_daily = pd.DataFrame({
    'sales':  np.random.randint(100, 500, 365),
    'visits': np.random.randint(1000, 5000, 365),
}, index=pd.date_range('2024-01-01', periods=365, freq='D'))

monthly_report = df_daily.resample('ME').agg({
    'sales':  ['sum', 'mean'],
    'visits': ['sum', 'max'],
})

时区处理

时区是时间序列处理中最复杂的部分之一,尤其是在处理跨时区系统日志、金融市场数据时。Pandas 通过 Python 的 zoneinfo(Python 3.9+)或第三方的 pytz 库支持时区。

import pandas as pd

# 创建 naive 时间序列
naive_ts = pd.Series(
    [1.0, 2.0, 3.0],
    index=pd.date_range('2024-01-01 08:00', periods=3, freq='h')
)
print(naive_ts.index.tz)  # None(无时区)

# 步骤1:localize——声明当前时间是哪个时区的
cst_ts = naive_ts.tz_localize('Asia/Shanghai')  # 中国标准时间(UTC+8)
print(cst_ts.index.tz)  # Asia/Shanghai

# 步骤2:convert——将 CST 时间转换为 UTC
utc_ts = cst_ts.tz_convert('UTC')
# 时间值会变化:08:00 CST → 00:00 UTC

# 步骤3:转换为其他时区
ny_ts = cst_ts.tz_convert('America/New_York')
# 北京上午8点 = 纽约前一天晚上7点(EST,UTC-5)

# 移除时区信息(变回 naive)
naive_back = cst_ts.tz_localize(None)

# read_csv 时直接指定时区
df = pd.read_csv('data.csv', parse_dates=['timestamp'])
df['timestamp'] = df['timestamp'].dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')

夏令时(DST)的处理

# 美国时区有夏令时(DST)问题
# 2024年 3月10日凌晨2点,美国东部时间拨快1小时(EST→EDT)

# 升采样时遇到 DST 会有缺失的小时
eastern = pd.date_range(
    '2024-03-09 23:00', '2024-03-10 04:00',
    freq='h', tz='America/New_York'
)
# 凌晨2点被跳过了(因为时钟跳到了3点)

# 解决方案:如有跨时区需求,数据存储为 UTC,显示时再转换
utc_range = pd.date_range('2024-03-09 23:00', periods=10, freq='h', tz='UTC')
# UTC 永远不会有 DST 问题

时间偏移与业务日历

import pandas as pd
from pandas.tseries.offsets import BDay, MonthEnd, QuarterEnd

# DateOffset:日历感知的时间偏移
today = pd.Timestamp('2024-03-15')

today + pd.DateOffset(months=1)   # 2024-04-15(一个月后)
today + pd.DateOffset(days=30)    # 2024-04-14(30天后)

# 工作日偏移(跳过周末)
today + BDay(5)   # 5个工作日后
today - BDay(3)   # 3个工作日前

# 月末和季末
today + MonthEnd(1)   # 2024-03-31(下一个月末)
today + QuarterEnd(1) # 2024-03-31(下一个季末)

# 判断是否是工作日
ts = pd.Timestamp('2024-03-16')  # 周六
print(ts.dayofweek)   # 5(周六)
print(ts.dayofweek < 5)  # False(不是工作日)

# 批量移动整列日期
df['due_date'] = df['order_date'] + pd.DateOffset(days=30)
df['next_business_day'] = df['date'] + BDay(1)

时间差(Timedelta)计算

import pandas as pd

df = pd.DataFrame({
    'order_date':   pd.to_datetime(['2024-01-01', '2024-01-05', '2024-01-10']),
    'deliver_date': pd.to_datetime(['2024-01-03', '2024-01-08', '2024-01-11']),
})

# 两个日期列相减,得到 Timedelta
df['delivery_days'] = df['deliver_date'] - df['order_date']
# dtype: timedelta64[ns]

# 提取天数(整数)
df['days_int'] = df['delivery_days'].dt.days

# 计算到"今天"的距离
today = pd.Timestamp.now()
df['days_ago'] = (today - df['order_date']).dt.days

# 判断是否超过了 SLA(如3天交付)
df['sla_met'] = df['days_int'] <= 3

时间序列的滚动计算(回顾与进阶)

import pandas as pd
import numpy as np

# 基于时间的滚动窗口(而非行数)
ts = pd.Series(
    np.random.randn(100),
    index=pd.date_range('2024-01-01', periods=100, freq='h')
)

# 基于时间窗口('24h' = 过去24小时,而非过去24行)
ts.rolling('24h').mean()   # 过去24小时的滑动均值
ts.rolling('7D').mean()    # 过去7天的滑动均值

# 基于偏移量的窗口(不规则时序)
irregular_ts = pd.Series(
    [1, 2, 3, 4],
    index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-05', '2024-01-10'])
)
# 基于行数的 rolling(3) 对不规则数据没有意义
# 基于时间的 rolling('3D') 更准确
irregular_ts.rolling('3D').mean()  # 每个点取过去3天的均值

# 分析实际股价数据(示例结构)
# price.rolling(20).mean()         # 20日均线(MA20)
# price.rolling(60).mean()         # 60日均线(MA60)
# price.pct_change().rolling(252).std() * 252**0.5  # 年化波动率

小结

Pandas 的时间序列功能是其核心优势之一: