4. 时间序列的切片与筛选

在 Pandas 中,时间索引(DatetimeIndex)一旦建立,就获得了比普通整数索引更丰富的切片能力。本节围绕三类最常用的筛选方式展开:基于时间标签的切片、基于布尔条件的区间筛选,以及按“每日时段”筛选的 between_time 与 at_time。最后用一个分钟级交易数据的实战任务,把这三类方法串起来,完成“提取开盘时段与收盘时段”的目标。

一、标签切片:df['2023-01':'2023-06'] 为什么能工作

当 DataFrame 的索引是 DatetimeIndex 且已排序时,Pandas 允许直接用日期字符串做切片。写法 df['2023-01':'2023-06'] 的含义是:从 2023-01-01 00:00:00 起,到 2023-06-30 23:59:59.999… 止(即 6 月整月)。这里有两个关键点需要先明确:

  • 左闭右闭:与普通 Python 切片 [a:b] 的左闭右开不同,时间标签切片两端都包含。写 '2023-01':'2023-06' 会包含 6 月的全部数据。
  • 部分字符串解析:只写年月时,Pandas 会自动补全为该月第一天的 00:00:00 作为起点、该月最后时刻作为终点。写 '2023-01-15' 则精确到当天。

切片能否生效,取决于索引是否单调递增。如果数据是乱序的,切片可能返回空结果或报错。因此实操第一步通常是 df = df.sort_index()。此外,若索引不是 DatetimeIndex,而是字符串列,则不能直接切片,需要先 pd.to_datetime 转换并 set_index。

import pandas as pd

# 假设 df 已按时间升序、索引为 DatetimeIndex
part = df['2023-01':'2023-06']          # 1 月至 6 月,两端都含
one_day = df['2023-03-15']              # 仅 3 月 15 日当天
precise = df['2023-03-15 09:30':'2023-03-15 11:30']  # 精确到分钟

标签切片适合“已知明确起止时间”的场景,写法简洁、可读性强。但它对索引有序性有要求,且不能表达“每天固定时段”这类循环条件——这正是后面两种方法要解决的问题。

二、布尔索引:表达任意时间条件

当筛选条件不是简单的连续区间,而是“某段时间内且满足其他条件”时,布尔索引更灵活。核心是把时间条件写成返回布尔 Series 的表达式,再用它做行选择。

mask = (df.index >= '2023-03-01') & (df.index < '2023-04-01')
march = df[mask]

# 组合业务条件:3 月内成交量大于 10000 的记录
march_big = df[mask & (df['volume'] > 10000)]

注意两点:一是多个条件必须用 &、| 并用括号包裹,不能用 and/or;二是比较时字符串会自动与时间戳比较,但为稳妥可显式写 pd.Timestamp('2023-03-01')。布尔索引的优势是条件可任意组合,劣势是写法比切片啰嗦,且每次都要构造完整布尔数组。

三、按每日时段筛选:between_time 与 at_time

金融、运维、日志类数据常需要“每天固定时段”的数据,例如每天 09:30–11:30 的交易。此时用日期切片无法表达,因为它是跨多天、每天重复的时段。Pandas 提供了两个专用方法:

  • df.between_time('09:30', '11:30'):保留每天该时段内的所有行,两端默认都包含。
  • df.at_time('09:30'):只保留每天恰好等于该时刻的行,常用于取开盘瞬间快照。

这两个方法只关心“时间部分”,忽略日期,因此要求索引为 DatetimeIndex。它们还支持 inclusive 参数控制端点是否包含,以及跨午夜时段(如 '22:00' 到 '02:00')的写法。与布尔索引相比,它们语义更清晰、代码更短,是“每日时段”场景的首选。

三类时间筛选方法的作用范围对比 同一份分钟级数据,三种写法各自“选中”哪些行 2023-01-01 2023-03-15 2023-06-30 df['2023-01':'2023-06'] 连续区间,左闭右闭,覆盖 1–6 月全部行 布尔索引 任意条件组合,如 3 月内且 volume>10000 between_time('09:30','11:30') 每天重复的时段,忽略日期,逐日命中 at_time('09:30') 每天恰好该时刻的单点快照

四、实战:从分钟级交易数据提取开盘与收盘时段

假设我们有一份 A 股某股票的分钟级成交数据,字段为 open, high, low, close, volume,索引为交易日的分钟时间戳,已按时间升序排列。A 股连续竞价时段为 09:30–11:30 和 13:00–15:00。任务要求:分别提取“开盘时段”(09:30–10:00)与“收盘时段”(14:30–15:00)的数据,并统计各自成交量。

import pandas as pd

# 1. 确保索引为 DatetimeIndex 且有序
df.index = pd.to_datetime(df.index)
df = df.sort_index()

# 2. 开盘时段:每天 09:30–10:00
open_period = df.between_time('09:30', '10:00')

# 3. 收盘时段:每天 14:30–15:00
close_period = df.between_time('14:30', '15:00')

# 4. 分别统计成交量
open_vol = open_period['volume'].sum()
close_vol = close_period['volume'].sum()

# 5. 取每天开盘第一分钟快照
open_snapshot = df.at_time('09:30')

执行后应如何判断结果是否正确?可以从三个角度检查:其一,open_period.index.time 的唯一值应全部落在 09:30–10:00 之间;其二,若数据覆盖 N 个交易日,open_snapshot 的行数应等于 N(每天恰好一条 09:30 记录,除非该日停牌或缺数据);其三,open_vol 与 close_vol 应小于全量成交量之和,且两者之和不应超过总量。若 at_time 返回行数明显少于交易日数,说明数据存在缺失分钟,需要回到数据清洗环节排查。

若数据包含 09:25 集合竞价记录,between_time('09:30','10:00') 不会包含它;如需纳入,可把起点改为 '09:25'。另外,between_time 默认两端都包含,若希望右端开区间,可传 inclusive='left'。

五、方法选择与常见陷阱

三类方法并非互斥,实际工作中常组合使用。选择依据可以概括为:连续日期区间用标签切片,复杂条件用布尔索引,每日固定时段用 between_time/at_time。若既要“某月”又要“每日某时段”,可先切片再 between_time,例如 df['2023-03'].between_time('09:30','10:00')。

常见陷阱包括:索引未排序导致切片结果异常;用 and 连接布尔条件报错;把 at_time 当成区间筛选(它只取单点);以及忽略时区——若索引带时区,between_time 的比较会按索引时区进行,跨时区数据需先统一。掌握这些边界,才能让时间筛选既写得短,又筛得准。