高效实现numpy数组匹配pandas DataFrame区间并获取对应行索引
原实现性能瓶颈说明
原双层遍历方案存在三个核心性能问题:
- 时间复杂度为O(N*M)(N为stats数组行数,M为DataFrame行数),没有利用两个输入已经提前完成升序排列的优势
- 全量循环都运行在Python解释器层,没有借助numpy/pandas的底层C层计算能力
- 每次循环用
x in range(start, end)做边界判断,哪怕range对象本身是轻量的,单次判断的Python层开销远高于直接做数值大小比较
优化方案
方案1:纯Numpy向量化广播(代码极简,适合中小数据量)
你的stats数组仅352行,属于极小量级,直接通过numpy广播一次性计算所有匹配关系,所有运算都在C层完成,比原循环快两个数量级以上。
代码示例:
import numpy as np import pandas as pd # 拆分stats前两列(x,y)为列向量,形状为(352, 1) x_col = stats[:, [0]] y_col = stats[:, [1]] # 拆分DataFrame四列边界为行向量,形状为(1, M) hpos_row = df["hpos"].to_numpy()[None, :] hpos_end_row = df["hpos_end"].to_numpy()[None, :] vpos_row = df["vpos"].to_numpy()[None, :] vpos_end_row = df["vpos_end"].to_numpy()[None, :] # 广播计算全量匹配条件:x落在[hpos, hpos_end) 且 y落在[vpos, vpos_end) # 若需要闭区间判断,将<替换为<=即可 x_match = (x_col >= hpos_row) & (x_col < hpos_end_row) y_match = (y_col >= vpos_row) & (y_col < vpos_end_row) full_match = x_match & y_match # 提取每个stats行对应的匹配DataFrame索引 matched_candidate = df.index[full_match.argmax(axis=1)] has_match = full_match.any(axis=1) # 构建最终结果:key为stats数组行号,value为匹配到的DataFrame行索引 matched_index_map = { stats_row: matched_candidate[stats_row] for stats_row in range(len(stats)) if has_match[stats_row] }
对于352行的stats数组,哪怕DataFrame有上万行,整体计算耗时也在毫秒级。
方案2:结合二分查找的升序优化(复杂度更低,适合大数据量)
利用两个输入已经完成升序的前置条件,通过二分查找快速缩小候选行范围,不需要遍历全量DataFrame,整体时间复杂度仅为O(N log M),数据量越大优势越明显:
# 提前将DataFrame列转为numpy数组,减少pandas索引开销 hpos_arr = df["hpos"].to_numpy() hpos_end_arr = df["hpos_end"].to_numpy() vpos_arr = df["vpos"].to_numpy() vpos_end_arr = df["vpos_end"].to_numpy() df_idx_arr = df.index.to_numpy() matched_index_map = {} for stats_row_idx, (x, y) in enumerate(stats[:, :2]): # 二分定位所有hpos <=x的DataFrame行的截止位置 h_upper_pos = hpos_arr.searchsorted(x, side="right") h_candidate_pos = np.arange(h_upper_pos) # 筛选满足hpos_end >x的行 h_valid_mask = hpos_end_arr[h_candidate_pos] > x valid_candidate = h_candidate_pos[h_valid_mask] if len(valid_candidate) == 0: continue # 在候选行中二分定位所有vpos <=y的位置(因df已按vpos升序,候选行的vpos自然有序) v_upper_pos = np.searchsorted(vpos_arr[valid_candidate], y, side="right") v_candidate_pos = valid_candidate[:v_upper_pos] # 筛选满足vpos_end >y的最终匹配行 v_valid_mask = vpos_end_arr[v_candidate_pos] > y if v_valid_mask.any(): matched_pos = v_candidate_pos[v_valid_mask][0] matched_index_map[stats_row_idx] = df_idx_arr[matched_pos]
关键注意点
- 原代码中
x in range(a, b)等价于左闭右开区间a <=x <b,若实际业务为闭区间判断,直接将代码中的<替换为<=即可。 - 因两个输入已经完成升序排序,上述方案不需要额外执行排序操作,直接复用现有顺序即可获得最高效率。
- 若存在单个stats行匹配多个DataFrame行的情况,可根据业务需求调整取数逻辑(默认取第一个匹配行,符合升序排列下的区间最近邻匹配逻辑)。
内容的提问来源于stack exchange,提问作者dataguy
相关产品推荐
相关产品推荐

