【问题标题】:Get the distance to each nearest element in a 1D/2D without for loop在没有 for 循环的情况下获取到 1D/2D 中每个最近元素的距离
【发布时间】:2020-10-06 23:08:49
【问题描述】:

我正在使用 numpy(也可能是 pandas 系列)在 python 中工作,并且正在尝试进行以下计算:

假设我有一个对应于 x 轴上的点的数组:

2, 9, 5, 6, 55, 8

对于这个数组中的每个元素,我想获取到最近元素的距离,因此输出如下所示:

3, 1, 1, 1, 46, 1

我正在尝试找到一种可以缩放到 2D(到最近的 XY 点的距离)并且理想情况下会避免 for 循环的解决方案。这可能吗?

【问题讨论】:

  • @jsotola 这不是真的。事实上,使用 numpy 循环很慢,而且通常是个坏主意。

标签: python pandas numpy


【解决方案1】:

这里似乎有一个 O(N^2) 解决方案的主题。对于 1D,得到 O(N log N) 非常简单:

x = np.array([2, 9, 5, 6, 55, 8])
i = np.argsort(x)
dist = np.diff(x[i])
min_dist = np.r_[dist[0], np.minimum(dist[1:], dist[:-1]), dist[-1]])
min_dist = min_dist[np.argsort(i)]

这显然不能很好地扩展到多个维度,因此请改用scipy.special.KDTree。假设您的数据是 N 维的并且形状为 (M, N),您可以这样做

k = KDTree(data)
dist = k.query(data, k=2)[0][:, -1]

Scipy 具有 KDTreecKDTree 的 Cython 实现。 Sklearn 有一个sklearn.neighbors.KDTree 也有类似的界面。

【讨论】:

  • @mathfux。谢谢你的收获。我还删除了一个多余的排序。
【解决方案2】:

方法 1

您可以使用广播来获取距离矩阵:

>>> data = np.array([2,9,5,6,55,8])
>>> dst_matrix = data - data[:, None]
>>> dst_matrix
array([[  0,   7,   3,   4,  53,   6],
       [ -7,   0,  -4,  -3,  46,  -1],
       [ -3,   4,   0,   1,  50,   3],
       [ -4,   3,  -1,   0,  49,   2],
       [-53, -46, -50, -49,   0, -47],
       [ -6,   1,  -3,  -2,  47,   0]])

然后我们可以按照in this post的建议消除对角线:

dst_matrix = dst_matrix[~np.eye(dst_matrix.shape[0],dtype=bool)].reshape(dst_matrix.shape[0],-1)
>>> dst_matrix
array([[  7,   3,   4,  53,   6],
       [ -7,  -4,  -3,  46,  -1],
       [ -3,   4,   1,  50,   3],
       [ -4,   3,  -1,  49,   2],
       [-53, -46, -50, -49, -47],
       [ -6,   1,  -3,  -2,  47]])

终于可以找到最少的物品了:

>>> np.min(np.abs(dst_matrix), axis=1)
array([ 3,  1,  1,  1, 46,  1])

方法 2 如果您正在寻找节省时间和内存的解决方案,最好的选择是scipy.spatial.cKDTrees,它将点(任何维度)打包到针对查询最近点进行优化的特定数据结构中。它还可以扩展到 2D 或 3D。

import scipy.spatial
data = np.array([2,9,5,6,55,8])

ckdtree = scipy.spatial.cKDTree(data[:,None])
distances, idx = ckdtree.query(data[:,None], k=2) 
output = distances[:,1] #distances to not coincident points

对于每个点,这里需要查询前两个最近的点,因为它们中的第一个预计是重合的。这是我在所有建议的答案之间找到的唯一解决方案,不需要很长时间(1M 点的平均性能是 4 秒)。 警告:您需要在应用此方法之前过滤重复点。

【讨论】:

    【解决方案3】:

    有很多方法可以实现它。一些可读和可概括的方式是:

    方法一

    dist = np.abs(a[:,None]-a)
    np.min(dist, where=~np.eye(len(a),dtype=bool), initial=dist.max(), axis=1)
    #[ 3  1  1  1 46  1]
    

    方法2

    dist = np.abs(np.subtract.outer(a,a))
    np.min(dist, where=~np.eye(len(a),dtype=bool), initial=dist.max(), axis=1)
    

    对于二维案例方法 1(假设欧几里得距离。任何其他距离也是可能的):

    from scipy.spatial.distance import cdist
    dist = cdist(a,a)
    np.min(dist, where=~np.eye(len(a),dtype=bool), initial=dist.max(), axis=1)
    

    对于仅使用 numpy 的二维案例方法 2

    dist=np.sqrt(((a[:,None]-a)**2).sum(-1))
    np.min(dist, where=~np.eye(len(a),dtype=bool), initial=dist.max(), axis=1)
    

    你可以实现faster distance calculation by using np.dot

    【讨论】:

    • 如果数组有重复值,这将失败,因为它们的距离将为零,这会从min 计算中过滤掉。试试:np.min(dist, where=np.eye(len(a))==0, initial=dist.max(), axis=0)
    • 这是 O(n^2)。
    • @MadPhysicist 好点。我也会添加一个 nlog(n) 解决方案。
    • 我刚发了一个
    • @MadPhysicist 是的,他确实想到了这一点。谢谢你。赞成。
    【解决方案4】:

    您可以对熊猫系列进行一些列表理解:

    s = pd.Series([2,9,5,6,55,8])
    s.apply(lambda x: min([abs(x - s[y]) for y in s.index if s[y] != x]))
    Out[1]: 
    0     3
    1     1
    2     1
    3     1
    4    46
    5     1
    

    那么你可以在末尾添加.to_list().to_numpy()来去掉系列索引:

    s.apply(lambda x: min([abs(x - s[y]) for y in s.index if s[y] != x])).to_numpy()
    
    array([ 3,  1,  1,  1, 46,  1], dtype=int64)
    

    【讨论】:

      猜你喜欢
      • 2020-04-07
      • 2020-04-22
      • 2013-10-24
      • 1970-01-01
      • 1970-01-01
      • 2019-09-02
      • 2022-06-13
      • 2021-09-27
      • 1970-01-01
      相关资源
      最近更新 更多