时间差与时区处理
本教程共 54 篇 · 第 47 篇 · 更新于 2026-08-11 · 约 8 分钟阅读
本节目标:学会用 Timedelta 做日期加减,用 shift 做数据滞后、diff 做差分,并搞懂 tz_localize 和 tz_convert 的时区处理。
Timedelta:时间的长度
两个时间点之间差多久?这就是 Timedelta(时间差)。它和 Timestamp 是配套的:Timestamp 是”几点几分”,Timedelta 是”多长时间”。
import pandas as pd
# 字符串创建
print(pd.Timedelta("1 days 2 hours"))
# 1 days 02:00:00
# 关键字参数创建
print(pd.Timedelta(days=1, seconds=1))
# 1 days 00:00:01
# 数字 + 单位
print(pd.Timedelta(90, unit="min"))
# 0 days 01:30:00
批量转换用 pd.to_timedelta:
import pandas as pd
print(pd.to_timedelta(["1 days 06:05:01", "15.5us", "nan"]))
# TimedeltaIndex(['1 days 06:05:01', '0 days 00:00:00.000015500', NaT], ...)
日期加减与求差
有了 Timedelta,日期运算就活了:
import pandas as pd
d = pd.Timestamp("2024-01-15")
print(d + pd.Timedelta(days=3))
# 2024-01-18 00:00:00
print(d - pd.Timedelta(days=5))
# 2024-01-10 00:00:00
# 两个时间点相减,得到 Timedelta
d1 = pd.Timestamp("2024-01-01")
d2 = pd.Timestamp("2024-01-15")
diff = d2 - d1
print(diff)
# 14 days 00:00:00
print(diff.days) # 14
print(diff.total_seconds()) # 1209600.0,总秒数
时间列也能整体加减:
import pandas as pd
s = pd.Series(pd.date_range("2024-01-01", periods=4, freq="D"))
print(s + pd.Timedelta(days=3))
# 0 2024-01-04
# 1 2024-01-05
# ...
print(s - s.min())
# 0 0 days
# 1 1 days
# 2 2 days
# 3 3 days
# dtype: timedelta64[us]
diff.days 只给出”整天”部分。时间差是”2 天 3 小时”时,.days 是 2,.seconds 是 10800(3 小时换算成秒),而不是把 3 小时折算成小数天。想拿到完整的”几小时几秒”,用 .components 查看各部分:
import pandas as pd
td = pd.Timedelta("2 days 3 hours 5 minutes")
print(td.components)
# Components(days=2, hours=3, minutes=5, seconds=0, ...)
Tip日期加减只认 Timedelta 和 DateOffset,不要直接
时间 + 3——3.0 里数字加法会直接报TypeError。加 1 天用pd.Timedelta(days=1),加 1 个月用pd.DateOffset(months=1)(日历运算,自动处理月末、闰年)。
shift:把数据整体挪动
shift 把数据在时间轴上”平移”:shift(1) 让每个值往后挪一位(滞后一期),最前面空出来一个位置;shift(-1) 往前挪一位(领先一期)。
import pandas as pd
s = pd.Series(range(1, 6))
print(s.shift(1))
# 0 NaN
# 1 1.0
# 2 2.0
# 3 3.0
# 4 4.0
print(s.shift(-1))
# 0 2.0
# 1 3.0
# 2 4.0
# 3 5.0
# 4 NaN
shift 在金融分析里是”昨日收盘价”的标准写法:df["昨收"] = df["收盘价"].shift(1)。挪出来的空位是 NaN,正好表示”没有前一天”。
shift 还有第二形态:shift(periods, freq="D") 不挪数据,而是把索引整体平移。索引是 DatetimeIndex 时,每个时间戳往后推 freq 指定的时长:
import pandas as pd
s = pd.Series([1, 2, 3], index=pd.date_range("2024-01-01", periods=3, freq="D"))
print(s.shift(1, freq="D"))
# 2024-01-02 1
# 2024-01-03 2
# 2024-01-04 3
数据没动,但日期往后挪了一天。这在”预测结果对位到未来日期”时很常用。
diff:差分
差分(diff)是 shift 最常用的搭档:每个值减去它前面那个值,算出一阶差分。其实 s.diff() 就是 s - s.shift(1) 的简写。
import pandas as pd
s = pd.Series([1, 3, 6, 10, 15])
print(s.diff())
# 0 NaN
# 1 2.0
# 2 3.0
# 3 4.0
# 4 5.0
# 隔 2 期差分:当前值减前 2 个值
print(s.diff(2))
# 0 NaN
# 1 NaN
# 2 5.0
# 3 7.0
# 4 9.0
# 百分比变化率
print(s.pct_change())
# 0 NaN
# 1 2.000000
# 2 1.000000
# 3 0.666667
# 4 0.500000
差分的作用是”去掉趋势看变化”:股价序列往往一路爬升,差分后变成每天的涨跌,平稳多了。diff(7) 算周同比(今天比上周今天),pct_change() 算涨跌幅。这三兄弟——shift、diff、pct_change——是时序特征工程的入门三件套。
DateOffset:日历运算
前面 TIP 里提过,加 1 个月要用 pd.DateOffset(months=1)。为什么不能直接用 Timedelta?因为月份长短不一:2 月 28 天、3 月 31 天,Timedelta 固定按秒算,无法表达”下个月同一天”这种日历语义。DateOffset 就是干这个的:
import pandas as pd
d = pd.Timestamp("2024-01-31")
print(d + pd.DateOffset(months=1))
# 2024-02-29 00:00:00(自动处理月末,2 月没有 31 号就落到 29 号)
# 工作日偏移:周五加 1 个工作日,直接跳到下周一
friday = pd.Timestamp("2024-01-05")
print(friday + pd.offsets.BDay())
# 2024-01-08 00:00:00
pd.offsets.BDay() 是”下一个工作日”,MonthEnd(1) 是”下个月最后一天”。做账务、排班这类依赖日历的业务,DateOffset 是标配。
时区:naive 与 aware
不带时区的时间叫 naive(朴素),带时区叫 aware(感知)。默认创建的时间都是 naive,dtype 是 datetime64[us]。带时区后 dtype 变成 datetime64[us, 时区名]。
时区有两个容易混的操作:
tz_localize:给 naive 时间”贴”一个时区。钟面数字不变,只是告诉 pandas”这个时间属于哪个时区”。tz_convert:把 aware 时间从 A 时区换算成 B 时区。这是同一个时刻,钟面数字会变。
import pandas as pd
# 不带时区的时间
naive = pd.date_range("2024-01-01 10:00", periods=3, freq="h")
print(naive)
# DatetimeIndex(['2024-01-01 10:00:00', ...], dtype='datetime64[us]', freq='h')
# 贴 UTC 时区:钟面还是 10 点
utc = naive.tz_localize("UTC")
print(utc)
# DatetimeIndex(['2024-01-01 10:00:00+00:00', ...], dtype='datetime64[us, UTC]', freq='h')
# 换算成上海时间:同一个时刻,钟面变成 18 点
print(utc.tz_convert("Asia/Shanghai"))
# DatetimeIndex(['2024-01-01 18:00:00+08:00', ...], dtype='datetime64[us, Asia/Shanghai]', freq='h')
记忆口诀:localize 是贴标签,convert 是换算。UTC 的 10 点,在上海看就是 18 点,两个时间相等——它们指向同一个瞬间。
时区在 Series 与 date_range 上的用法
Series 里的时间列,用 .dt 访问器操作时区;生成索引时可以直接带时区:
import pandas as pd
# date_range 直接创建带时区的索引
idx = pd.date_range("2024-01-01", periods=2, freq="D", tz="Asia/Shanghai")
print(idx)
# DatetimeIndex(['2024-01-01 00:00:00+08:00', '2024-01-02 00:00:00+08:00'],
# dtype='datetime64[us, Asia/Shanghai]', freq='D')
# Series 列:先贴 UTC,再换到上海
s = pd.Series(pd.date_range("2024-01-01", periods=2, freq="D"))
print(s.dt.tz_localize("UTC").dt.tz_convert("Asia/Shanghai"))
# 0 2024-01-01 08:00:00+08:00
# 1 2024-01-02 08:00:00+08:00
# dtype: datetime64[us, Asia/Shanghai]
两个不同时区的 Series 做运算,结果自动对齐到 UTC:
import pandas as pd
east = pd.Series(range(3), pd.date_range("2024-01-01", periods=3, tz="UTC")).tz_convert("US/Eastern")
berlin = pd.Series(range(3), pd.date_range("2024-01-01", periods=3, tz="UTC")).tz_convert("Europe/Berlin")
print(east + berlin)
# 2024-01-01 00:00:00+00:00 0
# ...
不管两个 Series 各自是什么时区,相加的结果索引统一是 UTC。内部存储也都是 UTC,时区只是显示层。
想脱掉时区:tz_localize(None) 去掉时区保留本地钟面;tz_convert(None) 先换算到 UTC 再去掉时区。
Warning最容易踩的坑是”时区混淆”:系统本地时间、服务器 UTC 时间、用户北京时间混在一起直接比较。统一的做法是:存数据一律用 UTC,展示时再 tz_convert 到目标时区。
夏令时:特殊情况
部分时区有夏令时(DST):春天时钟往前拨 1 小时,秋天往回拨 1 小时。这会产生两种诡异的时间:
- 不存在的时间:3 月的某个凌晨 2:30 被跳过了
- 重复的时间:11 月的某个 1:30 出现了两次
tz_localize 遇到这两种时间默认会报错。可以传 ambiguous(重复时间怎么处理:"infer" 按序列推断、"NaT" 置空)和 nonexistent(不存在的时间:"shift_forward" 前移、"NaT" 置空)来处理。中国没有夏令时,日常几乎碰不到;做欧美数据的跨时区分析时再深入研究。
小结
时间差这节收尾了时间序列的四件套:Timestamp 记时刻(§44)、DatetimeIndex 当索引(§45)、resample 换频率(§46)、Timedelta 算时长 + 时区转换(本节)。配合 shift、diff 做滞后与差分,时间序列的基本操作就齐了。下一节换个赛道,看看文本数据怎么用 .str 访问器批量处理。