Python快速计算多个距离

7

我有一个包含 36,742 个点的输入数据,这意味着如果我想计算距离矩阵(使用 Vincenty 近似)的下三角,则需要生成 36,742*36,741*0.5 = 1,349,974,563 个距离。

我希望保留在彼此之间距离在 50 公里以内的配对组合。我的当前设置如下:

shops= [[id,lat,lon]...]

def lower_triangle_mat(points):
    for i in range(len(shops)-1):
        for j in range(i+1,len(shops)):
            yield [shops[i],shops[j]]

def return_stores_cutoff(points,cutoff_km=0):
    below_cut = []
    counter = 0
    for x in lower_triangle_mat(points):
        dist_km = vincenty(x[0][1:3],x[1][1:3]).km
        counter += 1
        if counter % 1000000 == 0:
            print("%d out of %d" % (counter,(len(shops)*len(shops)-1*0.5)))
        if dist_km <= cutoff_km:
            below_cut.append([x[0][0],x[1][0],dist_km])
    return below_cut

start = time.clock()
stores = return_stores_cutoff(points=shops,cutoff_km=50)
print(time.clock() - start)

这显然需要花费数小时。我想到了一些可能性:

  • 使用numpy向量化这些计算,而不是通过循环进行
  • 使用某种哈希方法快速获得一个粗略的结果(100公里内的所有商店),然后只计算这些商店之间的准确距离
  • 不要将点存储在列表中,而是使用类似于四叉树的东西,但我认为这只有助于接近点的排名,而不是实际距离 -> 所以我猜测需要某种地理数据库
  • 我可以尝试使用haversine或投影并使用欧几里得距离,但我有兴趣使用可能最准确的度量标准
  • 利用并行处理(但我发现难以想出如何切割列表以仍然获得所有相关的对)。

编辑:我认为这里绝对需要geohashing - 例如此处的示例:

from geoindex import GeoGridIndex, GeoPoint

geo_index = GeoGridIndex()
for _ in range(10000):
    lat = random.random()*180 - 90
    lng = random.random()*360 - 180
    index.add_point(GeoPoint(lat, lng))

center_point = GeoPoint(37.7772448, -122.3955118)
for distance, point in index.get_nearest_points(center_point, 10, 'km'):
    print("We found {0} in {1} km".format(point, distance))

然而,我希望你能将地理哈希返回的商店的距离计算向量化(而不是循环)。

Edit2: Pouria Hadjibagheri - 我尝试使用lambda和map:

# [B]: Mapping approach           
lwr_tr_mat = ((shops[i],shops[j]) for i in range(len(shops)-1) for j in range(i+1,len(shops)))

func = lambda x: (x[0][0],x[1][0],vincenty(x[0],x[1]).km)
# Trying to see if conditional statements slow this down
func_cond = lambda x: (x[0][0],x[1][0],vincenty(x[0],x[1]).km) if vincenty(x[0],x[1]).km <= 50 else None

start = time.clock()
out_dist = list(map(func,lwr_tr_mat))
print(time.clock() - start)

start = time.clock()
out_dist = list(map(func_cond,lwr_tr_mat))
print(time.clock() - start)

他们都大约花费了61秒的时间(我将商店数量从32,000个限制为2,000个)。也许我使用地图不正确?


这些都听起来是不错的想法……问题是什么? - Emma
由于有太多的要点,我很难决定最好的行动方案,并希望得到一些指导,了解哪些是值得尝试的,哪些是浪费时间的。 - mptevsion
@Emma 哦,别这样!问题非常明确。 - Pouria
除非你有一台强大的计算机,否则你可能需要分块处理。存储这13亿个距离将需要超过10.5 G字节的内存。 - RootTwo
5个回答

7
这听起来像是k - D 树的典型应用案例。
如果你首先将点转换为欧几里得空间,那么可以使用 scipy.spatial.cKDTreequery_pairs 方法:
from scipy.spatial import cKDTree

tree = cKDTree(data)
# where data is (nshops, ndim) containing the Euclidean coordinates of each shop
# in units of km

pairs = tree.query_pairs(50, p=2)   # 50km radius, L2 (Euclidean) norm

pairs将是一个包含符合条件的商店行索引对 (i, j)set 集合,这些商店之间的距离小于等于50公里。


tree.sparse_distance_matrix 的输出结果是一个 scipy.sparse.dok_matrix。由于该矩阵是对称的,且您只关心唯一的行/列对,因此可以使用 scipy.sparse.tril 来将上三角区域归零,从而得到一个 scipy.sparse.coo_matrix。然后,您可以通过 .row.col.data 属性访问非零行和列索引及其对应的距离值:

from scipy import sparse

tree_dist = tree.sparse_distance_matrix(tree, max_distance=10000, p=2)
udist = sparse.tril(tree_dist, k=-1)    # zero the main diagonal
ridx = udist.row    # row indices
cidx = udist.col    # column indices
dist = udist.data   # distance values

啊,谢谢!这似乎是使用地理哈希的类似方法 - 你知道与它相比的优缺点吗? - mptevsion
1
说实话,我以前从未接触过地理哈希(我是神经科学家,不是地理学家!)。乍一看,你提供的geoindex模块似乎是用纯Python实现的,并需要在Python中循环遍历数组元素,而cKDTree是用C语言编写的,直接操作numpy数组。除了算法效率之外,由于具有更少的Python开销,我可能会预计cKDTree更快,但我对地理哈希算法的了解还不够,无法给出正确的答案。我建议你对它们进行基准测试并找出答案! - ali_m
阿里,你有没有关于如何将ckdtree稀疏距离矩阵保存为唯一的成对组合的建议?而不是矩阵。 - mptevsion
1
CSV 不是一个很好的输出格式选择。基于文本的格式需要更多的存储空间,通常比二进制格式(例如 numpy 的原生 .npy 格式)读写速度更慢,而且需要更多的内存。在浮点数据的情况下,它们还保证了一定的精度损失,因为使用有限长度的十进制字符串无法精确表示任意浮点数。基于文本的格式在数据需要可读性的情况下是最合适的选择,但我猜大多数人都不想阅读超过一百万行的浮点值... - ali_m
阿里,我刚刚注意到一个奇怪的问题;有一些观测值在500米内(例如),但是如果我设置1600米的截止,它们就会被删除。我尝试只对它们(例如5个点)运行整个程序,这样可以正常工作,所以我想知道是否有什么代码错误导致了这种情况?如果有帮助的话,我的最新“答案”包含了我的全部代码。我不太确定为什么会发生这种情况——毕竟500米并不比边界情况(1600)更近。 - mptevsion
显示剩余2条评论

1

你尝试过映射整个数组和函数而不是遍历它们吗?以下是一个示例:

from numpy.random import rand

my_array = rand(int(5e7), 1)  # An array of 50,000,000 random numbers in double.

现在通常做的是:

现在通常做的是:

squared_list_iter = [value**2 for value in my_array]

当然这可以运行,但是并不是最优解。
另一种方法是使用函数映射数组。操作如下:
func = lambda x: x**2  # Here is what I want to do on my array.

squared_list_map = map(func, test)  # Here I am doing it!

现在,有人可能会问,这有什么不同,甚至更好?因为现在我们还添加了对函数的调用!以下是你的答案:
对于前一种解决方案(通过迭代):
1 loop: 1.11 minutes.

与后一种解决方案(映射)相比:
500 loop, on average 560 ns. 

同时将 map() 转换为列表,使用 list(map(my_list)) 会使时间增加约10倍,达到大约 500毫秒
你来决定吧!

1
啊,这就是原因。时间花费在迭代上,而不是映射上。将您的矩阵重塑为n乘1,或将“每个”迭代作为应用于矩阵/数组的函数进行映射。 - Pouria
1
此外,安装jupyteripython以及函数%timeit来测试单独的代码行。这样更准确、更有信息量。 - Pouria
%timeit 运行一行代码通过循环(默认选择1到10k之间的最佳迭代次数)并取平均值。这更加准确,因为(1)它排除了调用函数所需的时间,以及(2)它排除了可能由于其他小操作在系统上运行而导致的时间变化。 - Pouria
如果您通过映射或使用numpy.reshape来完成,那么您可以使用var.tolist()函数获取一个list(),前提是varnumpy.array()的对象。 - Pouria
1
你会发现 reshape [ https://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.reshape.html#numpy.reshape ] 和 roll [ https://docs.scipy.org/doc/numpy-1.10.1/reference/generated/numpy.roll.html ] 很有帮助。 - Pouria
显示剩余7条评论

1
感谢大家的帮助。我认为我已经通过整合所有建议来解决了这个问题。
我使用numpy导入地理坐标,然后使用“法国兰伯特 - 93”进行投影。这使我可以用点填充scipy.spatial.cKDTree,然后通过指定50km的截止距离(我的投影点是以米为单位的)计算一个sparse_distance_matrix。然后我将下三角提取到CSV中。
import numpy as np
import csv
import time
from pyproj import Proj, transform

#http://epsg.io/2154 (accuracy: 1.0m)
fr = '+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 \
+x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 \
+units=m +no_defs'

#http://epsg.io/27700-5339 (accuracy: 1.0m)
uk = '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 \
+x_0=400000 +y_0=-100000 +ellps=airy \
+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs'

path_to_csv = '.../raw_in.csv'
out_csv = '.../out.csv'

def proj_arr(points):
    inproj = Proj(init='epsg:4326')
    outproj = Proj(uk)
    # origin|destination|lon|lat
    func = lambda x: transform(inproj,outproj,x[2],x[1])
    return np.array(list(map(func, points)))

tstart = time.time()

# Import points as geographic coordinates
# ID|lat|lon
#Sample to try and replicate
#points = np.array([
#        [39007,46.585012,5.5857829],
#        [88086,48.192370,6.7296289],
#        [62627,50.309155,3.0218611],
#        [14020,49.133972,-0.15851507],
#        [1091, 42.981765,2.0104902]])
#
points = np.genfromtxt(path_to_csv,
                       delimiter=',',
                       skip_header=1)

print("Total points: %d" % len(points))
print("Triangular matrix contains: %d" % (len(points)*((len(points))-1)*0.5))
# Get projected co-ordinates
proj_pnts = proj_arr(points)

# Fill quad-tree
from scipy.spatial import cKDTree
tree = cKDTree(proj_pnts)
cut_off_metres = 1600
tree_dist = tree.sparse_distance_matrix(tree,
                                        max_distance=cut_off_metres,
                                        p=2) 

# Extract triangle
from scipy import sparse
udist = sparse.tril(tree_dist, k=-1)    # zero the main diagonal
print("Distances after quad-tree cut-off: %d " % len(udist.data))

# Export CSV
import csv
f = open(out_csv, 'w', newline='') 
w = csv.writer(f, delimiter=",", )
w.writerow(['id_a','lat_a','lon_a','id_b','lat_b','lon_b','metres'])
w.writerows(np.column_stack((points[udist.row ],
                             points[udist.col],
                             udist.data)))
f.close()

"""
Get ID labels
"""
id_to_csv = '...id.csv'
id_labels = np.genfromtxt(id_to_csv,
                       delimiter=',',
                       skip_header=1,
                       dtype='U')

"""
Try vincenty on the un-projected co-ordinates
"""
from geopy.distance import vincenty
vout_csv = '.../out_vin.csv'
test_vin = np.column_stack((points[udist.row].T[1:3].T,
                            points[udist.col].T[1:3].T))

func = lambda x: vincenty(x[0:2],x[2:4]).m
output = list(map(func,test_vin))

# Export CSV
f = open(vout_csv, 'w', newline='')
w = csv.writer(f, delimiter=",", )
w.writerow(['id_a','id_a2', 'lat_a','lon_a',
            'id_b','id_b2', 'lat_b','lon_b',
            'proj_metres','vincenty_metres'])
w.writerows(np.column_stack((list(id_labels[udist.row]),
                             points[udist.row ],
                             list(id_labels[udist.col]),
                             points[udist.col],
                             udist.data,
                             output,
                             )))

f.close()    
print("Finished in %.0f seconds" % (time.time()-tstart)

这种方法生成所需时间为164秒(针对5,306,434个距离)-相比于9秒-并且约需要90秒保存到磁盘。
然后我比较了Vincenty距离和投影坐标上的直角距离之间的差异。
平均差异为2.7米,平均差异/米为0.0073%-看起来非常好。

0
“使用某种哈希算法快速获取(100公里内的所有商店)的粗略数据,然后只计算这些商店之间的准确距离。” 我认为这可能更好地称为网格化。首先创建一个字典,以一组坐标作为键,并将每个商店放入靠近该点的50公里桶中。然后在计算距离时,您只需要查看附近的桶,而不是遍历整个宇宙中的每个商店。”

0

你可以使用向量化的方式,并结合此线程中讨论的 Haversine 公式 Python 中的哈弗辛公式(两个 GPS 点之间的方位和距离)

lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2    
c = 2 * np.arcsin(np.sqrt(a))
km = 6371 * c

这里有%%timeit用于计算7,451,653个距离

642 ms ± 20.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接