1.8 时间序列处理
第 5 章接触过日期时间列的 .dt 访问器,但真实的时间序列分析还要解决更多问题:如何生成连续的日期序列、如何把字符串高效转成时间戳、如何按时间段聚合数据、如何处理时区。Pandas 对时间序列有完整支持,包含 Timestamp、Period、Timedelta 三类时间对象以及一整套频率、偏移、重采样工具。本节系统讲解时间序列的核心内容。
1.8.1 日期范围生成
pd.date_range()
pd.date_range() 生成连续的 DatetimeIndex,指定起止或长度:
import pandas as pd
print(pd.date_range('2024-01-01', periods=5))
print(pd.date_range('2024-01-01', '2024-01-05'))
print(pd.date_range('2024-01-01', periods=5, freq='D'))
print(pd.date_range('2024-01-01', periods=3, freq='M')) # 按月freq 参数控制间隔,常用取值 'D'(天)、'H'(小时)、'M'(月末)、'Y'(年末)、'W'(周)。
pd.bdate_range()
pd.bdate_range() 生成工作日(周一至周五)日期范围,跳过周末:
print(pd.bdate_range('2024-01-01', periods=5))pd.period_range()
pd.period_range() 生成 PeriodIndex(时间段):
print(pd.period_range('2024-01', periods=3, freq='M'))
# PeriodIndex(['2024-01', '2024-02', '2024-03'], dtype='period[M]')pd.timedelta_range()
pd.timedelta_range() 生成 TimedeltaIndex(时间差范围):
print(pd.timedelta_range('1 day', periods=3, freq='D'))
# TimedeltaIndex(['1 days', '2 days', '3 days'])1.8.2 时间戳
pd.Timestamp
pd.Timestamp 表示单个时间点:
ts = pd.Timestamp('2024-01-15 08:30:00')
print(ts)
print(ts.year, ts.month, ts.day) # 2024 1 15
print(ts + pd.Timedelta(days=1)) # 时间运算pd.to_datetime()
pd.to_datetime() 把多种形式转成时间戳,前面已接触,这里补充参数:
s = pd.Series(['2024-01-01', '2024/02/15', '20240103'])
print(pd.to_datetime(s))
print(pd.to_datetime(s, format='%Y%m%d', errors='coerce'))format 指定解析格式可加速并避免歧义,errors='coerce' 把非法日期转成 NaT。
1.8.3 时间周期
pd.Period 与 pd.PeriodIndex
pd.Period 表示一段固定长度的时间段:
p = pd.Period('2024-03', freq='M')
print(p) # 2024-03
print(p + 1) # 2024-04pd.PeriodIndex 是周期索引,.to_period() 把时间戳转成周期:
ts = pd.to_datetime(['2024-01-15', '2024-03-20'])
print(ts.to_period('M'))
# PeriodIndex(['2024-01', '2024-03'], dtype='period[M]')周期与时间戳的区别:时间戳是时刻,周期是区间。月度周期 2024-03 代表整个三月。
1.8.4 时间差
pd.Timedelta 与 pd.to_timedelta()
pd.Timedelta 表示时间差,pd.to_timedelta() 把字符串转成时间差:
td = pd.Timedelta('2 days 3 hours')
print(td) # 2 days 03:00:00
print(td.total_seconds()) # 秒数
s = pd.Series(['1 day', '2 hours'])
print(pd.to_timedelta(s))时间戳相减得到 Timedelta,时间差参与日期运算:
t1 = pd.Timestamp('2024-01-10')
t2 = pd.Timestamp('2024-01-15')
print(t2 - t1) # 5 days1.8.5 时间序列索引
DatetimeIndex、PeriodIndex、TimedeltaIndex 可以作为 DataFrame 的行索引,启用时间序列的整套能力:
idx = pd.date_range('2024-01-01', periods=4, freq='D')
df = pd.DataFrame({'值': [10, 20, 15, 25]}, index=idx)
print(df)
print(df.index) # DatetimeIndex以时间索引后,可以按时间段切片、重采样、移位。
1.8.6 频率与偏移量
freq 描述时间序列的步长,底层由 pd.offsets 模块的偏移对象实现。常用偏移量:
from pandas.tseries.offsets import (Day, Hour, Minute, Second,
MonthEnd, MonthBegin, QuarterEnd,
YearEnd, BusinessDay)
print(pd.Timestamp('2024-01-15') + Day(2)) # 加 2 天
print(pd.Timestamp('2024-01-15') + MonthEnd(1)) # 加到下一个月底
print(pd.Timestamp('2024-01-15') + BusinessDay(1)) # 加 1 个工作日偏移量可以直接相加、组合,也可以作为 freq 参数传入 date_range 和 resample。MonthEnd、QuarterEnd、YearEnd 这类锚定偏移会跳到最近的周期末尾,第 21 章详细展开。
1.8.7 时间序列移位 .shift()
.shift() 把数据沿时间轴整体平移,periods 控制移动的步数:
s = pd.Series([10, 20, 30], index=pd.date_range('2024-01-01', periods=3))
print(s.shift(1)) # 向后移 1 步,开头变 NaN
print(s.shift(-1)) # 向前移 1 步,末尾变 NaN
print(s.shift(1, freq='D')) # 同时移动时间索引freq 参数会同时移动索引时间,这在把数据对齐到新时间点时很有用。
1.8.8 时区处理
.dt.tz_localize() 设定时区,.dt.tz_convert() 转换时区:
s = pd.to_datetime(['2024-01-01 08:00:00'])
s_utc = s.dt.tz_localize('UTC')
print(s_utc)
print(s_utc.dt.tz_convert('Asia/Shanghai'))
# 2024-01-01 16:00:00+08:00tz_localize 给无时区的数据标注时区,tz_convert 在不同时区之间转换时刻(绝对时间不变)。整个索引的时区处理用 df.index.tz_localize()。
1.8.9 重采样 .resample()
.resample() 按频率聚合时间序列。降采样(高频转低频)时聚合,升采样(低频转高频)时填充:
idx = pd.date_range('2024-01-01', periods=90, freq='D')
df = pd.DataFrame({'值': range(90)}, index=idx)
# 降采样:按周求均值
print(df.resample('W').mean())
# 降采样:按月求和
print(df.resample('M').sum())
# 降采样:按季度取最大值
print(df.resample('Q').max())on 参数指定按某列重采样(该列需是时间类型),level 指定按索引层级重采样:
df2 = pd.DataFrame({'日期': pd.date_range('2024-01-01', periods=30), '值': range(30)})
print(df2.resample('M', on='日期').sum())升采样(如按天采样到按小时)会产生缺失,需要填充:
daily = df.resample('D').sum()
hourly = daily.resample('h').ffill() # 升采样后前向填充1.8.10 移动平均与差分
移动平均
滚动窗口计算移动平均是时间序列平滑的常用手段:
df['移动平均'] = df['值'].rolling(7).mean()差分 .diff()
.diff() 计算相邻时间点的差值,用于去除趋势:
print(df['值'].diff())
print(df['值'].diff(2)) # 相隔 2 个时间点的差值1.8.11 时间序列对齐
时间序列之间运算时按时间索引自动对齐,两侧时间标签不匹配的部分得到 NaN:
s1 = pd.Series([1, 2], index=pd.to_datetime(['2024-01-01', '2024-01-02']))
s2 = pd.Series([10, 20], index=pd.to_datetime(['2024-01-02', '2024-01-03']))
print(s1 + s2)
# 2024-01-01 NaN
# 2024-01-02 22.0
# 2024-01-03 NaN练习题
第1题 概念理解
说明 pd.Timestamp、pd.Period、pd.Timedelta 三者的区别;说明降采样与升采样的区别。
参考答案
pd.Timestamp 是时刻,pd.Period 是一段时间区间,pd.Timedelta 是时间差。降采样从高频聚合到低频(按周、月、季汇总),升采样从低频扩展到高频(需要填充缺失)。
第2题 代码编写
用 pd.date_range 生成 2024 年每天的时间索引;构造 DataFrame 后用 .resample('M') 按月求和;用 .shift(1) 生成滞后一列;用 .diff() 计算逐日差值。
参考答案
import pandas as pd
idx = pd.date_range('2024-01-01', '2024-12-31', freq='D')
df = pd.DataFrame({'值': range(1, 367)}, index=idx)
print(df.resample('M').sum())
df['滞后'] = df['值'].shift(1)
df['差分'] = df['值'].diff()
print(df.head())第3题 进阶练习
把字符串日期列转成 datetime 并设为索引;用 .to_period('M') 生成月份周期;用 pd.bdate_range 生成工作日序列并与完整日期序列对比长度;对时间序列用 .rolling(7).mean() 计算周移动平均。
参考答案
import pandas as pd
df = pd.DataFrame({'日期': ['2024-01-01', '2024-01-08'], '值': [5, 10]})
df['日期'] = pd.to_datetime(df['日期'])
df = df.set_index('日期')
print(df.index.to_period('M'))
bdays = pd.bdate_range('2024-01-01', '2024-01-31')
print(len(bdays)) # 少于 31
s = pd.Series(range(30), index=pd.date_range('2024-01-01', periods=30))
print(s.rolling(7).mean())常见错误
错误 1 · 时间列还是字符串时直接重采样报 TypeError
原因:.resample() 要求索引或 on 指定的列是时间类型,字符串列无法重采样。
解决:先用 pd.to_datetime() 转类型,并确认索引是 DatetimeIndex。
错误 2 · 重采样结果的时间标签与预期不符
原因:resample 默认标签对齐方式与 closed/label 参数有关,例如 'M' 落在月末。
解决:用 label='left'/label='right' 调整标签位置,用 closed 调整区间端点。
错误 3 · 升采样后全是 NaN
原因:低频数据升采样到高频,中间位置没有值,默认产生 NaN。
解决:接上 .ffill()、.bfill() 或 .interpolate() 填充。
错误 4 · .shift() 后第一行出现 NaN
原因:移位后开头没有可用的前值,自然产生缺失。
解决:这是正常行为,后续用 fillna 处理,或设置 fill_value 参数。