NumPy 高性能技巧:基于多列条件查找最近邻行索引的向量化实现

聖光之護
发布: 2025-11-29 13:39:16
原创
670人浏览过

NumPy 高性能技巧:基于多列条件查找最近邻行索引的向量化实现

本文深入探讨如何在numpy中高效解决一个复杂的数据查询问题:为大型数组中的每一行,查找满足特定多列(第二列和第四列)值完全匹配,且在另一列(第一列)上距离最近的n个行的原始索引。通过摒弃低效的python for循环,本教程将详细展示如何利用numpy的向量化操作,包括数据预处理、巧妙的分块策略、广播机制进行距离计算与排序,以及结果整合与还原,从而实现显著的性能提升和代码的简洁性,为处理大规模数据集提供一个专业且可复用的解决方案。

问题背景与挑战

在数据分析和科学计算中,我们经常需要从大型数据集中检索满足特定条件的记录。一个常见的挑战是,当查询条件涉及多列,并且需要根据某一列的“距离”来找出最接近的N个记录时,传统的逐行迭代(例如使用Python的for循环)会带来严重的性能问题,尤其是在处理百万级别甚至更大规模的NumPy数组时。

具体而言,我们的目标是:给定一个具有8列的NumPy数组,对于数组中的每一行,我们需要找出其他满足以下两个条件的N行:

  1. 它们的第二列(索引为1)的值与当前行的第二列值完全相同。
  2. 它们的第四列(索引为3)的值与当前行的第四列值完全相同。
  3. 在满足上述两个条件的所有行中,其第一列(索引为0)的值与当前行的第一列值最接近。

最终输出应是一个与原始数组行数相同、列数为N的数组,其中每一行包含对应原始行的N个最近邻行的原始索引。

NumPy 向量化解决方案概览

为了克服for循环的性能瓶颈,我们将采用NumPy的向量化能力。核心思路是:

  1. 添加原始索引:在对数组进行任何排序或分块操作之前,保留每行的原始位置信息。
  2. 按条件列分组:将原始数组根据其中一个等值条件列(例如第二列)进行排序,然后分块。这样,每个子块内部的所有行的该条件列值都是相同的。
  3. 在组内过滤与计算:对于每个子块,再根据另一个等值条件列(第四列)进行过滤。在过滤后的数据中,使用NumPy的广播机制高效计算第一列的绝对差值,并找出N个最小差值的索引。
  4. 整合结果:将所有子块计算出的索引合并,并根据原始索引恢复数组的初始顺序。

详细实现步骤

1. 准备数据:添加原始索引

在进行任何可能改变行顺序的操作之前,我们需要为原始数组的每一行添加一个唯一的标识符,即其在原始数组中的索引。这确保了即使数据经过排序、过滤和分块,我们也能最终追溯到其在原始数据集中的位置。

笔魂AI
笔魂AI

笔魂AI绘画-在线AI绘画、AI画图、AI设计工具软件

笔魂AI 403
查看详情 笔魂AI
import numpy as np

# 假设 arr 是从 "data.txt" 加载的原始大型数组
# arr = np.loadtxt("data.txt")

# 示例数据(简化版,模拟原始数据结构)
# 假设原始数据有8列,这里只用4列进行演示
arr = np.array([
    [1.0, 0.0, -1.6, 2.3],  # idx 0
    [1.0, 1.0, -1.6, 2.3],  # idx 1
    [1.0, 0.0, -1.6, 2.3],  # idx 2 (col1=0, col4=2.3, col0=1.0)
    [2.0, 0.0, -1.6, 2.3],  # idx 3
    [1.1, 0.0, -1.6, 2.3],  # idx 4 (col1=0, col4=2.3, col0=1.1)
    [1.2, 0.0, -1.6, 2.3],  # idx 5 (col1=0, col4=2.3, col0=1.2)
    [1.0, 1.0, -1.6, 2.3],  # idx 6 (col1=1, col4=2.3, col0=1.0)
    [1.1, 1.0, -1.6, 2.3],  # idx 7 (col1=1, col4=2.3, col0=1.1)
    [1.0, 0.0, -1.6, 7.3],  # idx 8 (col1=0, col4=7.3, col0=1.0)
    [1.1, 0.0, -1.6, 7.3]   # idx 9 (col1=0, col4=7.3, col0=1.1)
])

# 添加原始索引作为最后一列
# np.c_ 用于按列堆叠数组
arr_indexed = np.c_[arr, np.arange(len(arr))]
print("原始数组(带索引):\n", arr_indexed)
登录后复制

在上述代码中,np.arange(len(arr)) 生成一个从0到 len(arr)-1 的整数序列,然后 np.c_ 将其作为新的一列添加到原始数组 arr 的右侧。

2. 按条件列排序与分块

为了高效处理“第二列和第四列值相同”的条件,我们可以首先对数组进行排序。选择第二列(索引为1)作为主要的排序键,因为它的值在问题描述中是离散的。排序后,所有第二列值相同的行将聚集在一起,这为后续的分块操作提供了便利。

# 按第二列的值进行排序
arr_sorted_by_col1 = arr_indexed[arr_indexed[:, 1].argsort()]
print("\n按第二列排序后的数组:\n", arr_sorted_by_col1)

# 根据第二列的唯一值进行分块
# np.unique(..., return_index=True) 返回唯一值及其首次出现的索引
unique_col1_values, split_indices = np.unique(arr_sorted_by_col1[:, 1], return_index=True)
# split_indices[1:] 提供了除第一个块之外的所有分界点
splitted_array = np.split(arr_sorted_by_col1, split_indices[1:])

print("\n按第二列分块后的数组(列表形式):")
for i, block in enumerate(splitted_array):
    print(f"--- 块 {i} (第二列值为 {block[0, 1]}) ---\n", block)
登录后复制

np.unique(arr_sorted_by_col1[:, 1], return_index=True)[1][1:] 能够找到第二列值发生变化的所有位置,这些位置作为 np.split 的参数,可以将排序后的数组精确地分割成多个子数组,每个子数组内部的第二列值都是相同的。

3. 核心逻辑:query_trks 函数

这个函数负责在每个分块(即第二列值已确定的子数组)内部,进一步根据第四列的值进行过滤,然后计算第一列的距离并找出N个最近邻的原始索引。

def query_trks(FULL_block, target_col4_value, N):
    """
    在给定的数据块中,查找满足第四列条件且第一列最接近的N个行的原始索引。

    参数:
    FULL_block (np.ndarray): 一个NumPy数组,其中第二列的值是恒定的。
                            最后一列包含原始索引。
    target_col4_value (float): 要匹配的第四列值。
    N (int): 需要查找的最近邻行数。

    返回:
    np.ndarray: 一个数组,包含N个最近邻行的原始索引。
                如果符合条件的行数不足N,则返回所有符合条件的行的索引。
    """
    # 过滤出第四列值与目标值相等的行
    # 注意:这里假设第四列是索引为4,但根据问题描述,它是索引3(0-based)
    # 在示例数据中,我们假设是索引3
    # 根据问题描述,是第四列(索引3)
    filt_mask = (FULL_block[:, 3] == target_col4_value)

    # 获取过滤后的行的索引(在FULL_block内部的相对索引)
    internal_filt_indices = np.where(filt_mask)[0]

    if len(internal_filt_indices) == 0:
        return np.array([], dtype=int) # 没有匹配的行

    # 提取过滤后的行的第一列值
    trks_col0 = FULL_block[internal_filt_indices, 0]

    # 使用广播机制计算所有过滤后的行之间第一列值的绝对差值
    # trks_col0[:, None] 将一维数组转换为列向量,与 trks_col0 进行元素级减法
    # 得到一个 (len(trks_col0), len(trks_col0)) 的差值矩阵
    diff_matrix = np.abs(trks_col0[:, None] - trks_col0)

    # 对差值矩阵按列排序,获取排序后的索引
    # argsort(axis=0) 对每一列进行排序,返回排序后的索引
    # [:N] 获取前N个最小差值的索引
    # 注意:这里需要处理自身与自身的距离为0的情况,通常我们希望排除自身
    # 一种方法是先将对角线元素设为无穷大,或者在获取N个索引后排除自身
    # 考虑到实际需求,通常N个最近邻会包含自身(距离为0),如果需要排除,需额外处理。
    # 这里的argsort会把0距离的自身排在第一位。

    # 为了排除自身,我们可以将对角线元素设置为一个大值
    np.fill_diagonal(diff_matrix, np.inf)

    # 重新排序获取前N个最小差值的索引
    # argsort(axis=0) 会对每一列进行排序,返回排序后的索引
    # [:N] 获取前N个最小差值的索引
    closest_internal_indices = diff_matrix.argsort(axis=0)[:N]

    # 将内部索引转换回 FULL_block 中的实际索引
    # 然后再通过 FULL_block 找到其原始索引

    # 最终结果需要针对每个原始行返回 N 个索引。
    # 这里的 diff_matrix 是针对 trks_col0 的,所以 closest_internal_indices 也是 trks_col0 的索引。
    # 我们需要的是 FULL_block 中对应行的原始索引。

    # 创建一个空的列表来存储每个查询行的N个最近邻原始索引
    result_indices = np.empty((len(internal_filt_indices), N), dtype=int)

    for i, current_row_internal_idx in enumerate(internal_filt_indices):
        # 对于当前行 (FULL_block[current_row_internal_idx]),
        # 找到其在 trks_col0 中的位置 (j)
        # diff_matrix 的第 j 列包含了当前行与其他行的距离

        # 找到当前行在 trks_col0 内部的索引
        j = np.where(internal_filt_indices == current_row_internal_idx)[0][0]

        # 获取第 j 列中 N 个最近邻的内部索引
        n_closest_in_trks_col0 = closest_internal_indices[:, j]

        # 将这些内部索引映射回 FULL_block 的内部索引
        n_closest_in_full_block = internal_filt_indices[n_closest_in_trks_col0]

        # 提取这些行对应的原始索引(最后一列)
        result_indices[i] = FULL_block[n_closest_in_full_block, -1].astype(int)

    return result_indices

# 假设 N = 2
N = 2

# 创建一个列表来存储每个分块计算出的结果
all_computed_indices = []

# 遍历每个分块并调用 query_trks
for block in splitted_array:
    # 在每个block中,第二列的值是相同的,我们只需获取其值
    # 并且对于block中的每一行,我们都需要找到N个最近邻

    # 假设我们为block中的每一行查找最近邻
    # 我们需要迭代block中的每一行作为“当前行”来查询

    # 获取 block 中所有行的原始索引,这些是需要填充结果的行
    original_indices_in_block = block[:, -1].astype(int)

    # 为当前 block 创建一个临时存储结果的数组
    block_results = np.empty((len(block), N), dtype=int)

    for i, row in enumerate(block):
        # 当前行的第二列值和第四列值
        current_col1_val = row[1]
        current_col4_val = row[3]
        current_col0_val = row[0]

        # 过滤出与当前行第四列值相同的行
        # 这里的 FULL_block 实际上就是当前的 block,因为我们已经在外部按 col1 分割了
        # 并且 query_trks 应该只返回与 target_col4_value 匹配的行的最近邻

        # 优化:query_trks 应该接收一个 block 和一个 target_col4_value
        # 并且它应该为 block 中所有满足 target_col4_value 的行计算最近邻

        # 重新设计 query_trks,使其能够处理一个 block 中的所有行
        # 原始的 query_trks 假设 `trk` 是一个值,而不是整个列
        # 让我们回到原始答案的逻辑,它通过 `a[:, 0]` 传入了整个第一列,
        # 这意味着 `query_trks` 内部会为 `trks` 中的每个元素找到最近邻。
        # 这里的 `trks` 是 `FULL[filt, 0]`,即过滤后的第一列。

        # 让我们按照答案的逻辑重新构建 query_trks
        # `query_trks(FULL, trk, v, N)`
        # `FULL` 是当前的 `block`
        # `trk` 在答案中被移除,因为 `trks = FULL[filt, 0]` 已经包含了所有需要比较的第一列值
        # `v` 是 `target_col4_value`
登录后复制

以上就是NumPy 高性能技巧:基于多列条件查找最近邻行索引的向量化实现的详细内容,更多请关注php中文网其它相关文章!

数码产品性能查询
数码产品性能查询

该软件包括了市面上所有手机CPU,手机跑分情况,电脑CPU,电脑产品信息等等,方便需要大家查阅数码产品最新情况,了解产品特性,能够进行对比选择最具性价比的商品。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号