【问题标题】:Efficiently get minimum values for each pair of elements from two arrays in a third array有效地从第三个数组中的两个数组中获取每对元素的最小值
【发布时间】:2017-08-25 18:12:04
【问题描述】:

我有两个 N 浮点数组(充当 (x,y) 坐标并且可能有重复)和关联的 z 数组 N 浮点数(充当坐标的权重)。

对于每对 (x,y) 浮点数,我需要选择具有最小关联 z 值的对。我已经定义了一个 selectMinz() 函数来执行此操作(请参见下面的代码),但它需要的时间太长。

如何提高此功能的性能?

import numpy as np
import time


def getData():
    N = 100000
    x = np.arange(0.0005, 0.03, 0.001)
    y = np.arange(6., 10., .05)
    # Select N values for x,y, where values can be repeated
    x = np.random.choice(x, N)
    y = np.random.choice(y, N)
    z = np.random.uniform(10., 15., N)
    return x, y, z


def selectMinz(x, y, z):
    """
    Select the minimum z for each (x,y) pair.
    """
    xy_unq, z_unq = [], []
    # For each (x,y) pair
    for i, xy in enumerate(zip(*[x, y])):
        # If this xy pair was already stored in the xy_unq list
        if xy in xy_unq:
            # If the stored z value associated with this xy pair is
            # larger than this new z[i] value
            if z_unq[xy_unq.index(xy)] > z[i]:
                # Store this smaller value instead
                z_unq[xy_unq.index(xy)] = z[i]
        else:
            # Store the xy pair, and its associated z value
            xy_unq.append(xy)
            z_unq.append(z[i])

    return xy_unq, z_unq


# Define data with the proper format.
x, y, z = getData()

s = time.clock()
xy_unq, z_unq = selectMinz(x, y, z)  # <-- TAKES TOO LONG (~15s in my system)
print(time.clock() - s)

【问题讨论】:

    标签: python arrays performance numpy


    【解决方案1】:

    步骤:

    1. 使用lex-sort 使x-y 对按顺序出现。或者,我们可以使用缩放方法将其中一个数组按另一个数组的值范围进行缩放,然后将其与另一个数组相加,最后使用argsort 得到 lex 排序的等效索引。
    2. 使用np.minimum.reduceat 获取由对分组定义的区间中的最小值。

    因此,我们将有一个矢量化解决方案,就像这样 -

    def selectMinz_vectorized(x, y, z):
        # Get grouped lex-sort indices
        sidx = (y + x*(y.max() - y.min() + 1)).argsort()
        # or sidx = np.lexsort([x, y])
    
        # Lex-sort x, y, z
        x_sorted = x[sidx]
        y_sorted = y[sidx]
        z_sorted = z[sidx]
    
        # Get equality mask between each sorted X and Y elem against previous ones.
        # The non-zero indices of its inverted mask gives us the indices where the 
        # new groupings start. We are calling those as cut_idx.
        seq_eq_mask = (x_sorted[1:] == x_sorted[:-1]) & (y_sorted[1:] == y_sorted[:-1])
        cut_idx = np.flatnonzero(np.concatenate(( [True], ~seq_eq_mask)))
    
        # Use those cut_idx to get intervalled minimum values
        minZ = np.minimum.reduceat(z_sorted, cut_idx)
    
        # Make tuples of the groupings of x,y and the corresponding min Z values
        return (zip(x_sorted[cut_idx], y_sorted[cut_idx]), minZ.tolist())
    

    示例运行 -

    In [120]: np.c_[x,y,z]
    Out[120]: 
    array([[  0.,   1.,  69.],
           [  2.,   0.,  47.],
           [  1.,   0.,  62.],
           [  0.,   2.,  33.],
           [  1.,   7.,  32.],
           [  1.,   0.,  50.],
           [  2.,   0.,  55.]])
    
    In [121]: selectMinz(x,y,z) # original method
    Out[121]: 
    ([(0.0, 1.0), (2.0, 0.0), (1.0, 0.0), (0.0, 2.0), (1.0, 7.0)],
     [69.0, 47.0, 50.0, 33.0, 32.0])
    
    In [122]: selectMinz_vectorized(x,y,z)
    Out[122]: 
    ([(1.0, 0.0), (2.0, 0.0), (0.0, 1.0), (0.0, 2.0), (1.0, 7.0)],
     [50.0, 47.0, 69.0, 33.0, 32.0])
    

    这是我最初的方法,它涉及创建一个堆叠数组,然后执行这些操作。实现看起来像这样 -

    def selectMinz_vectorized_v2(x, y, z):
        d = np.column_stack((x,y,z))
        sidx = np.lexsort(d[:,:2].T)
        b = d[sidx]  
        cut_idx = np.r_[0,np.flatnonzero(~(b[1:,:2] == b[:-1,:2]).all(1))+1]
        minZ = np.minimum.reduceat(b[:,-1], cut_idx)
        return ([tuple(i) for i in b[cut_idx,:2]], minZ.tolist())
    

    向量化方法的基准测试

    方法-

    # Pruned version of the approach posted earlier
    def selectMinz_vectorized_pruned(x, y, z):
        sidx = (y + x*(y.max() - y.min() + 1)).argsort()
        x_sorted = x[sidx]
        y_sorted = y[sidx]
        z_sorted = z[sidx]
        seq_eq_mask = (x_sorted[1:] == x_sorted[:-1]) & (y_sorted[1:] == y_sorted[:-1])
        cut_idx = np.flatnonzero(np.concatenate(( [True], ~seq_eq_mask)))
        minZ = np.minimum.reduceat(z_sorted, cut_idx)
        return x_sorted[cut_idx], y_sorted[cut_idx], minZ
    
    def numpy_indexed_app(x,y,z): # @Eelco Hoogendoorn's soln
        return npi.group_by((x, y)).min(z)
    

    时间安排 -

    In [141]: x,y,z=getData(10000)
    
    In [142]: %timeit selectMinz_vectorized_pruned(x, y, z)
         ...: %timeit numpy_indexed_app(x,y,z)
         ...: 
    1000 loops, best of 3: 763 µs per loop
    1000 loops, best of 3: 1.09 ms per loop
    
    In [143]: x,y,z=getData(100000)
    
    In [144]: %timeit selectMinz_vectorized_pruned(x, y, z)
         ...: %timeit numpy_indexed_app(x,y,z)
         ...: 
    100 loops, best of 3: 8.53 ms per loop
    100 loops, best of 3: 12.9 ms per loop
    

    【讨论】:

    • 我知道会有一种矢量化的方式 :) 虽然这种语法很笨拙,但我的时间是 10 loops, best of 3: 79.4 ms per loop 用于 dict 方法,10 loops, best of 3: 25.6 ms per loop 用于此。我想知道熊猫在这里竞争是否有希望?这是将语法上干净的函数翻译成的那种东西吗?
    • 我不确定如何解压缩函数返回的数据。能否让它以与原始函数相同的格式返回数据?
    • 太好了,谢谢!虽然这个功能(对我来说)比 Antimony 的功能难读 100 倍,但它的速度也快了 2 倍以上。因此,我选择它。再次感谢!
    • 我可以确认这个新功能比旧功能快了近 2 倍,而且可读性更强。干得好!
    • @NimishBansal 只需在这里回答 MATLAB/NumPy 问题并尽可能避免循环;)
    【解决方案2】:

    xy_unqz_unq 的数据结构更改为包含这两条信息的字典,使我的系统上的时间从~7 秒缩短到~0.1 秒。

    def selectMinz(x, y, z):
        """
        Select the minimum z for each (x,y) pair.
        """
        xy_unq = {}
        # For each (x,y) pair
        for i, xy in enumerate(zip(*[x, y])):
            # If this xy pair was already stored in the xy_unq list
            if xy in xy_unq:
                # If the stored z value associated with this xy pair is
                # larger than this new z[i] value
                if xy_unq[xy] > z[i]:
                    # Store this smaller value instead
                    xy_unq[xy] = z[i]
            else:
                # Store the xy pair, and its associated z value
                xy_unq[xy] = z[i]
    
        return xy_unq.keys(), xy_unq.values()
    

    对我来说,上述方法的时间范围从 ~0.106s 到 ~0.11s。这是另一种方法,代码行数更少,但需要更多时间(~0.14):

    def selectMinz(x, y, z):
        """
        Select the minimum z for each (x,y) pair.
        """
        xy_unq = {}
        # For each (x,y) pair
        for i, xy in enumerate(zip(*[x, y])):
            # If this xy pair was already stored in the xy_unq list
            if xy in xy_unq:
                # Store the value that is smaller between the current stored value and the new z[i]
                xy_unq[xy] = min(xy_unq[xy], z[i])
            else:
                # Store the xy pair, and its associated z value
                xy_unq[xy] = z[i]
    
        return xy_unq.keys(), xy_unq.values()
    

    【讨论】:

    • 您将z 值存储在xy_unq 中,这是不正确的。
    • 我将xy 存储为键,并将其关联的最小值z 存储为值。如果您需要所有z 的最终列表,您可以通过调用xy_unq.values() 轻松获得。
    • @Gabriel 我觉得有一种矢量化方法可以解决这个问题,我只是不知道(并且想要).... 这种方法非常好(赞成)但它失败了首先是 numpy 的要点。我建议您暂时保留这个问题。
    • @Gabriel 去吧!
    • 这是一个很好的答案,比 Divakar 的答案更具可读性。因为我需要性能最好的那个,所以我选择了另一个答案,但非常感谢你的回答!
    【解决方案3】:

    numpy_indexed 包(免责声明:我是它的作者)包含以优雅高效的方式解决此类分组问题的功能:

    import numpy_indexed as npi
    xy_unq, z_unq = npi.group_by((x, y)).min(z)
    

    【讨论】:

    • 这是一个非常优雅的解决方案!您是否根据 Divakar 的代码对其进行了计时?
    • @Gabriel 已添加到我的帖子中。
    • 这很好。有没有理由不能成为 numpy 的一部分?从@Divakar 给出的时间安排来看,它肯定可以与自定义方法竞争,语法类似于 Pandas
    • 谢谢。幕后采取的步骤在概念上与 Divakar 的步骤相同。但为了便于概括,这里和那里有一点开销。我最初写这篇文章是为了改进 numpy;但是更多地研究它,我觉得 numpy 强加的微妙的向后兼容性要求太严格了;而且我有点喜欢快速迭代这组功能的能力,而不是受限于它们的发布周期。
    • 也就是说,numpy 设法从这个包向后移植的越多越好。朝着正确方向迈出的一步是新的 numpy 支持在 np.unique 中有一个轴 kwarg,因此您可以找到唯一的行等等,而不必自己破解它。但是我觉得一个坚实的group-by绝对应该在numpy的范围内;这种数据争吵更多的是 numpy 的核心,而不是 ffts。 OTOH,如今的包管理非常好,可以说将 numpy 拆分为更多的微包更有意义;像这样的东西就是其中之一。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-03
    • 1970-01-01
    • 1970-01-01
    • 2014-12-07
    • 1970-01-01
    • 2021-10-05
    • 1970-01-01
    相关资源
    最近更新 更多