时间序列数据的重要性
时间序列数据是现代数据分析中最常见的数据类型之一:股票价格、服务器监控指标、电商销售数据、用户行为日志……几乎所有业务系统都在实时产生时间序列数据。Pandas 提供了业界最完整的时间序列处理工具箱,这也是它在金融分析领域广受欢迎的重要原因。
时间相关的核心类型
- Timestamp 单个时间点,Pandas 中时间数据的基本单位,是 Python datetime 的增强版,支持纳秒级精度(numpy.datetime64 底层)。
- DatetimeIndex 由 Timestamp 组成的索引,是时间序列 Series/DataFrame 的行索引。支持字符串切片、时区感知、resample 等专用操作。
- Period / PeriodIndex 表示时间"区间"(如 2024 年 1 月这整个月),而非时间点。适合财务报表、统计报告等以时期为单位的数据。
- Timedelta / TimedeltaIndex 时间差,表示两个时间点之间的差值(如 3天4小时)。支持算术运算(时间戳 + 时间差 = 新时间戳)。
- DateOffset 日历感知的时间偏移(如"下一个工作日"、"月末"),处理业务时间逻辑时比 Timedelta 更合适。
创建时间序列数据
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)是时间序列中最核心的操作之一,分为两种:
- 降采样(Downsampling) 从高频率→低频率聚合,如日频→月频。需要指定聚合函数(sum/mean/max等)将多个数据点合并为一个。
- 升采样(Upsampling) 从低频率→高频率插值,如月频→日频。需要指定如何填充新增的空白点(ffill/bfill/interpolate)。
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 库支持时区。
- Naive datetime 不含时区信息的时间戳,只是一个"朴素"的年月日时分秒数字,不知道属于哪个时区。
- Timezone-aware datetime 含时区信息的时间戳,知道自己处于哪个时区,可以正确地转换为其他时区,也可以与 UTC 时间相互转换。
- UTC(协调世界时) 国际标准时间,无时区偏移。存储时间序列数据的最佳实践是使用 UTC,显示时再转换为用户本地时区。
- tz_localize 为 naive datetime 指定一个时区(不改变时间值,只是"声明"它所在的时区)。
- tz_convert 将 timezone-aware datetime 转换为另一个时区(改变时间值和时区信息,保持绝对时刻不变)。
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 的时间序列功能是其核心优势之一:
- DatetimeIndex 让时间索引的切片和选择变得极为直观(直接用字符串如 '2024-Q1')
- resample() 是降采样/升采样的核心工具,注意 Pandas 2.2 的频率别名变更
- 时区处理:存储用 UTC,显示时用 tz_convert 转换,避免 DST 问题
- .dt 访问器提供了丰富的日期时间分量提取能力
- 基于时间窗口的
rolling('7D')比基于行数的rolling(7)对不规则数据更准确