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')的写法。与布尔索引相比,它们语义更清晰、代码更短,是“每日时段”场景的首选。
四、实战:从分钟级交易数据提取开盘与收盘时段
假设我们有一份 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 返回行数明显少于交易日数,说明数据存在缺失分钟,需要回到数据清洗环节排查。
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 的比较会按索引时区进行,跨时区数据需先统一。掌握这些边界,才能让时间筛选既写得短,又筛得准。