时间索引与日期范围
本教程共 54 篇 · 第 45 篇 · 更新于 2026-08-11 · 约 8 分钟阅读
本节目标:学会用 date_range 生成日期范围、把日期做成索引,并掌握按时间切片、按年月筛选数据的几种写法。
DatetimeIndex 是什么
上一节把字符串转成了时间类型,但那些时间还只是”数据”。时间序列数据的惯例是:把时间放到索引上。一行数据代表”这个时刻发生了什么”,时间戳就是这行的门牌号。
由一组 Timestamp 组成的索引,就是 DatetimeIndex(时间索引):
import pandas as pd
idx = pd.DatetimeIndex(["2024-01-01", "2024-01-02", "2024-01-03"])
print(idx)
# DatetimeIndex(['2024-01-01', '2024-01-02', '2024-01-03'], dtype='datetime64[us]', freq=None)
# 带时间索引的 Series
s = pd.Series([100, 120, 110], index=idx)
print(s)
# 2024-01-01 100
# 2024-01-02 120
# 2024-01-03 110
# dtype: int64
日常不用手动拼索引。读完数据后 df.set_index("日期") 把日期列设为索引(§23),或者读文件时用 parse_dates + index_col,就能得到带时间索引的表格。
date_range:批量生成日期
手工列日期太笨。pd.date_range 按指定频率自动生成一串日期,是时间序列最常用的工具之一:
import pandas as pd
# 从 2024-01-01 开始,生成 5 天(默认按天)
print(pd.date_range("2024-01-01", periods=5, freq="D"))
# DatetimeIndex(['2024-01-01', ..., '2024-01-05'], dtype='datetime64[us]', freq='D')
# 指定结束日期
print(pd.date_range("2024-01-01", "2024-01-05"))
# 同样得到 5 天
# 每小时
print(pd.date_range("2024-01-01", periods=3, freq="h"))
# DatetimeIndex(['2024-01-01 00:00:00', '2024-01-01 01:00:00', '2024-01-01 02:00:00'], ...)
# 每个自然月最后一天
print(pd.date_range("2024-01-01", periods=3, freq="ME"))
# DatetimeIndex(['2024-01-31', '2024-02-29', '2024-03-31'], ...)
start、end、periods 三个参数给任意两个,第三个自动推出来。freq 是频率,用别名表示。常用的几个:
| 别名 | 含义 |
|---|---|
| D | 每天 |
| h / min / s | 每小时 / 每分钟 / 每秒 |
| B | 每个工作日 |
| W | 每周(默认周日结束,W-MON 表示周一结束) |
| MS / ME | 每月第一天 / 每月最后一天 |
| QS / QE | 每季第一天 / 最后一天 |
| YS / YE | 每年第一天 / 最后一天 |
Warning老教程里常见的
freq="M"(月尾)在 pandas 3.0 已经改名成"ME","MS"仍是月初。见到旧代码里的 “M”,记得换成 “ME”。频率还可以叠加,比如"2h30min"表示每 2 小时 30 分钟一个点。
bdate_range 是 date_range 的工作日版本,自动跳过周六日:
import pandas as pd
print(pd.bdate_range("2024-01-04", periods=5))
# 2024-01-04(周四)到 2024-01-10(周三),跳过 6、7 号周末
还有个细节:start 不在频率的”锚点”上时,date_range 会滚动到下一个有效点。比如频率是 MS(月初),start 是 1 月 15 日,生成结果从 2 月 1 日开始。end 同理,会停在最后一个有效点之前。
按时间选择数据
索引建好了,怎么取数据?核心规则:用 .loc 传日期或日期字符串。字符串切片两端都包含,和普通标签切片一个逻辑:
import pandas as pd
idx = pd.date_range("2024-01-01", periods=100, freq="D")
s = pd.Series(range(100), index=idx)
# 取单个日期(精确命中返回标量)
print(s.loc["2024-01-10"])
# 9
# 取一个区间:两端都包含
print(s.loc["2024-01-05":"2024-01-10"])
# 2024-01-05 4
# 2024-01-06 5
# ...
# 2024-01-10 9
# 只写年和月:筛出整个 3 月
print(s["2024-03"])
# 2024-03-01 60
# 2024-03-02 61
# ...
“2024-03” 这种只写一部分的字符串,叫部分字符串索引(partial string indexing)。pandas 会把它理解成”2024-03-01 到 2024-03-31 整个区间”,一次取出一个月的数据。对 DataFrame 同样适用:
import pandas as pd
import numpy as np
df = pd.DataFrame(
{"销售额": np.random.randint(100, 500, 100)},
index=pd.date_range("2024-01-01", periods=100, freq="D"),
)
# 取 1 月的数据
print(df.loc["2024-01"])
# 取 1 月 5 日到 1 月 10 日
print(df.loc["2024-01-05":"2024-01-10"])
部分字符串索引还有一个”精度规则”:字符串比索引精度粗,就按切片处理(返回多条);和索引精度一致或更细,就按精确匹配处理(返回一条)。
Warning对 DataFrame 用
df["2024-01"]这种写法取行已经失效(会直接报 KeyError),因为和取列名有歧义。取行一律用.loc:df.loc["2024-01"]。
时间索引的常用操作
时间索引继承了普通索引的所有能力,还多了几个时间专属的便利:
import pandas as pd
idx = pd.date_range("2024-01-01", periods=5, freq="D")
s = pd.Series(range(5), index=idx)
print(s.index.freq) # 频率信息:<Day>,自动识别
print(s.index.min()) # 最早时间
print(s.index.max()) # 最晚时间
print(s.index.year) # 批量取年份
# Index([2024, 2024, 2024, 2024, 2024], dtype='int64')
print(s.index.month) # 批量取月份
# Index([1, 1, 1, 1, 1], dtype='int64')
# 索引整体平移 2 天(得到新的索引)
print(s.index + pd.Timedelta(days=2))
# DatetimeIndex(['2024-01-03', '2024-01-04', '2024-01-05', '2024-01-06', '2024-01-07'], ...)
index.freq 很有用:如果 pandas 能推断出等间隔频率,它会显示成 <Day> 之类的对象,后面重采样(§46)时频率信息会直接派上用场。数据不是等间隔的,freq 就是 None。
另外,DatetimeIndex 支持大部分普通索引操作,比如 isin、unique、sort_values,还支持按日期做布尔筛选:
import pandas as pd
s = pd.Series(range(5), index=pd.date_range("2024-01-01", periods=5, freq="D"))
# 只保留工作日(周一至周五中的日期)
print(s[s.index.dayofweek < 5])
Warning时间索引的顺序很重要。多数时间操作(切片、重采样、rolling)都假设索引是升序的。索引乱序时,先
sort_index()再操作。另外,索引里有重复时间戳时,部分字符串切片的结果可能和直觉不符,建议先duplicated()检查。
时间索引的运算
时间索引可以直接做算术,结果还是时间索引:
import pandas as pd
idx = pd.date_range("2024-01-01", periods=3, freq="D")
# 索引整体加 2 天
print(idx + pd.Timedelta(days=2))
# DatetimeIndex(['2024-01-03', '2024-01-04', '2024-01-05'], dtype='datetime64[us]', freq='D')
# 两个索引相减,得到时间差索引
print(idx - pd.date_range("2024-01-10", periods=3, freq="D"))
# TimedeltaIndex(['-9 days', '-9 days', '-9 days'], dtype='timedelta64[us]', freq=None)
这类运算最常见的用途是算”距今多少天”:pd.Timestamp.today() - s.index。
truncate:截取时间区间
truncate 是切片的一个便捷替身,只保留指定区间内的数据:
import pandas as pd
import numpy as np
s = pd.Series(
np.random.randn(60),
index=pd.date_range("2024-01-01", periods=60, freq="D"),
)
print(s.truncate(before="2024-02-01", after="2024-02-15"))
truncate 和切片有个细微差别:切片遇到”2024-02”会包含 2 月里所有匹配的日子,truncate 把未写出的部分当 0 处理,只精确匹配。日常切片更常用,truncate 偶尔能省事。
时区基础
时间索引可以带上时区。date_range 里直接给 tz 参数,生成的索引就带时区信息:
import pandas as pd
print(pd.date_range("2024-01-01", periods=2, freq="D", tz="Asia/Shanghai"))
# DatetimeIndex(['2024-01-01 00:00:00+08:00', '2024-01-02 00:00:00+08:00'],
# dtype='datetime64[us, Asia/Shanghai]', freq='D')
注意 dtype 变成了 datetime64[us, Asia/Shanghai],多出的部分是时区名。时区的核心操作有两个:tz_localize(给不带时区的时间”贴上”时区)和 tz_convert(把带时区的时间从 A 时区换算到 B 时区)。它们很容易混,细节放到 §47 专门讲,这里先记住:date_range 支持 tz 参数,就能创建带时区的时间索引。
小结
DatetimeIndex 是时间序列的骨架:date_range 生成日期、set_index 挂到索引、loc 按时间切片、部分字符串按月筛选。配合 §44 的 to_datetime,读入的原始数据就能变成可切可查的时间序列。下一节在这个骨架上做频率转换——重采样。