You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何向量化Pandas中同ID指定日期范围的历史状态查询

高效实现同ID指定时间范围内的最后状态查询

你遇到的问题非常典型——循环遍历大DataFrame的每一行进行切片查询,时间复杂度是O(n²),数据量上去之后肯定会慢到离谱。下面我会给你两种向量化/高效的实现方式,都是基于Pandas的原生操作,性能会提升几个数量级。

方法一:利用merge_asof(最简洁高效)

merge_asof是Pandas专门用来按时间进行近似匹配的工具,非常适合这种"找同组内时间范围内最近的前序记录"的场景。步骤如下:

  1. 先对数据按分组键和时间排序:merge_asof要求左右表都按by(分组列)和on(时间列)排序,这是前提。
  2. 构造匹配用的右表:复制原数据并重命名相关列,方便后续匹配。
  3. 设置时间范围条件:通过计算左表的时间上下限,结合merge_asof的参数实现范围筛选。

具体代码:

import pandas as pd
from datetime import timedelta

# 假设你的原始DataFrame是df,包含created_dt, number_id, status列
window_size = 15
gap_size = 5

# 1. 对原始数据按number_id和created_dt排序
df_sorted = df.sort_values(['number_id', 'created_dt']).reset_index(drop=True)

# 2. 构造右表:复制排序后的df,重命名列用于匹配
right_df = df_sorted.copy()
right_df = right_df.rename(columns={
    'created_dt': 'prev_created_dt',
    'status': 'prev_status'
})

# 3. 计算左表的时间匹配范围:右表的prev_created_dt需要 >= 当前行created_dt - (window+gap),且 < 当前行created_dt - gap
# 先给左表添加时间上下限列
df_sorted['upper_bound'] = df_sorted['created_dt'] - timedelta(days=gap_size)
df_sorted['lower_bound'] = df_sorted['created_dt'] - timedelta(days=window_size + gap_size)

# 4. 使用merge_asof进行匹配:按number_id分组,按created_dt匹配,找小于upper_bound的最近记录,同时确保>=lower_bound
result = pd.merge_asof(
    df_sorted,
    right_df,
    by='number_id',
    left_on='upper_bound',
    right_on='prev_created_dt',
    direction='backward',  # 找小于等于left_on的最大的right_on值
    allow_exact_matches=False  # 排除刚好等于upper_bound的记录(因为我们要<upper_bound)
)

# 5. 筛选出在lower_bound范围内的记录,超出范围的设为None
result['prev_status'] = result.apply(
    lambda x: x['prev_status'] if x['prev_created_dt'] >= x['lower_bound'] else None,
    axis=1
)

# 最后可以把不需要的辅助列删掉,恢复原始顺序(如果需要的话)
result = result.drop(['upper_bound', 'lower_bound', 'prev_created_dt'], axis=1)
# 如果需要回到原始df的顺序,可以用原始索引合并
# result = result.set_index(df.index).sort_index()

这个方法的时间复杂度是O(n log n)(主要来自排序),比循环的O(n²)快太多,百万级数据也能轻松处理。

方法二:分组+二分查找(适合需要更精细控制的场景)

如果你对merge_asof的参数不太熟悉,也可以用分组结合二分查找的方式,利用组内时间有序的特性快速定位目标记录:

import pandas as pd
from datetime import timedelta
import bisect

window_size = 15
gap_size = 5

# 1. 按number_id分组,每个组内按created_dt排序
df_sorted = df.sort_values(['number_id', 'created_dt']).reset_index(drop=True)
groups = df_sorted.groupby('number_id')

# 2. 为每个组预处理时间戳列表和status列表,方便二分查找
group_data = {}
for num_id, group in groups:
    # 把时间转换成timestamp(数值类型,方便二分)
    timestamps = group['created_dt'].apply(lambda x: x.timestamp()).tolist()
    statuses = group['status'].tolist()
    group_data[num_id] = (timestamps, statuses)

# 3. 定义函数,根据当前行的信息查找符合条件的status
def get_prev_status(row):
    num_id = row['number_id']
    current_dt = row['created_dt']
    upper_ts = (current_dt - timedelta(days=gap_size)).timestamp()
    lower_ts = (current_dt - timedelta(days=window_size + gap_size)).timestamp()
    
    timestamps, statuses = group_data[num_id]
    # 找到第一个>=upper_ts的索引,那么前一个就是<=upper_ts的最大索引
    upper_idx = bisect.bisect_left(timestamps, upper_ts)
    if upper_idx == 0:
        # 没有比upper_ts小的记录
        return None
    # 找到第一个>=lower_ts的索引
    lower_idx = bisect.bisect_left(timestamps, lower_ts)
    if upper_idx - 1 < lower_idx:
        # 时间范围内没有记录
        return None
    # 返回范围内最后一个status(也就是upper_idx-1位置的)
    return statuses[upper_idx - 1]

# 4. 批量应用函数
df_sorted['prev_status'] = df_sorted.apply(get_prev_status, axis=1)

# 同样可以恢复原始顺序
# df_sorted = df_sorted.set_index(df.index).sort_index()

这个方法的核心是用二分查找(bisect模块)代替每次切片,每组内的查找是O(log m)(m是组内行数),整体复杂度也是O(n log n),性能和merge_asof差不多,但是逻辑更直观,方便自定义调整条件。

为什么原代码慢?

原代码的问题在于:

  • 每次循环都要对整个DataFrame进行切片筛选(df[(df.created_dt >= since) & ...]),这会生成新的DataFrame,非常耗时。
  • 循环遍历itertuples本身在大数据集下效率就很低,Pandas的优势是向量化操作,要尽量避免逐行循环。

两种方法都避免了逐行切片,利用排序和Pandas的高效分组/匹配操作,性能提升非常明显。

内容的提问来源于stack exchange,提问作者gonzadevelop

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:30:54