Эффективный способ вычисления матрицы расстояний для данных широты и долготы в Python
у меня есть данные для широты и долготы, и мне нужно рассчитать матрицу расстояний между двумя массивами, содержащими местоположения. Я использовал это этой чтобы получить расстояние между двумя точками с учетом широты и долготы.
вот пример моего кода:
import numpy as np
import math
def get_distances(locs_1, locs_2):
n_rows_1 = locs_1.shape[0]
n_rows_2 = locs_2.shape[0]
dists = np.empty((n_rows_1, n_rows_2))
# The loops here are inefficient
for i in xrange(n_rows_1):
for j in xrange(n_rows_2):
dists[i, j] = get_distance_from_lat_long(locs_1[i], locs_2[j])
return dists
def get_distance_from_lat_long(loc_1, loc_2):
earth_radius = 3958.75
lat_dif = math.radians(loc_1[0] - loc_2[0])
long_dif = math.radians(loc_1[1] - loc_2[1])
sin_d_lat = math.sin(lat_dif / 2)
sin_d_long = math.sin(long_dif / 2)
step_1 = (sin_d_lat ** 2) + (sin_d_long ** 2) * math.cos(math.radians(loc_1[0])) * math.cos(math.radians(loc_2[0]))
step_2 = 2 * math.atan2(math.sqrt(step_1), math.sqrt(1-step_1))
dist = step_2 * earth_radius
return dist
мой ожидаемый результат таков:
>>> locations_1 = np.array([[34, -81], [32, -87], [35, -83]])
>>> locations_2 = np.array([[33, -84], [39, -81], [40, -88], [30, -80]])
>>> get_distances(locations_1, locations_2)
array([[ 186.13522573, 345.46610882, 566.23466349, 282.51056676],
[ 187.96657622, 589.43369894, 555.55312473, 436.88855214],
[ 149.5853537 , 297.56950329, 440.81203371, 387.12153747]])
производительность важна для меня, и одна вещь, которую я мог бы сделать, это использовать Cython
для ускорения циклов, но было бы неплохо, если бы у меня не было идти туда.
есть ли модуль, который может сделать что-то подобное? Или любое другое решение?
4 ответов
есть много неоптимальных вещей в гаверсинус уравнений вы используете. Вы можете обрезать некоторые из них и минимизировать количество синусов, косинусов и квадратных корней, которые вам нужно рассчитать. Следующее-лучшее, что я смог придумать, и в моей системе работает примерно в 5 раз быстрее, чем код Ophion (который делает в основном то же самое, что и векторизация) на двух случайных массивах 1000 и 2000 элементов:
def spherical_dist(pos1, pos2, r=3958.75):
pos1 = pos1 * np.pi / 180
pos2 = pos2 * np.pi / 180
cos_lat1 = np.cos(pos1[..., 0])
cos_lat2 = np.cos(pos2[..., 0])
cos_lat_d = np.cos(pos1[..., 0] - pos2[..., 0])
cos_lon_d = np.cos(pos1[..., 1] - pos2[..., 1])
return r * np.arccos(cos_lat_d - cos_lat1 * cos_lat2 * (1 - cos_lon_d))
если вы кормите его своими двумя массивами "как есть", он будет жалуйтесь, но это не ошибка, это особенность. В принципе, эта функция вычисляет расстояние на сфере над последним измерением и передает на остальных. Так что вы можете получить то, что вы после того как:
>>> spherical_dist(locations_1[:, None], locations_2)
array([[ 186.13522573, 345.46610882, 566.23466349, 282.51056676],
[ 187.96657622, 589.43369894, 555.55312473, 436.88855214],
[ 149.5853537 , 297.56950329, 440.81203371, 387.12153747]])
но его также можно использовать для вычисления расстояний между двумя списками точек, т. е.:
>>> spherical_dist(locations_1, locations_2[:-1])
array([ 186.13522573, 589.43369894, 440.81203371])
или между двумя отдельными точками:
>>> spherical_dist(locations_1[0], locations_2[0])
186.1352257300577
это вдохновляет на то, как работают gufuncs, и как только вы привыкнете к этому, я нашел, что это замечательно стиль кодирования "swiss army knife", который позволяет повторно использовать одну функцию во множестве различных настроек.
Это просто векторизация вашего кода:
def new_get_distances(loc1, loc2):
earth_radius = 3958.75
locs_1 = np.deg2rad(loc1)
locs_2 = np.deg2rad(loc2)
lat_dif = (locs_1[:,0][:,None]/2 - locs_2[:,0]/2)
lon_dif = (locs_1[:,1][:,None]/2 - locs_2[:,1]/2)
np.sin(lat_dif, out=lat_dif)
np.sin(lon_dif, out=lon_dif)
np.power(lat_dif, 2, out=lat_dif)
np.power(lon_dif, 2, out=lon_dif)
lon_dif *= ( np.cos(locs_1[:,0])[:,None] * np.cos(locs_2[:,0]) )
lon_dif += lat_dif
np.arctan2(np.power(lon_dif,.5), np.power(1-lon_dif,.5), out = lon_dif)
lon_dif *= ( 2 * earth_radius )
return lon_dif
locations_1 = np.array([[34, -81], [32, -87], [35, -83]])
locations_2 = np.array([[33, -84], [39, -81], [40, -88], [30, -80]])
old = get_distances(locations_1, locations_2)
new = new_get_distances(locations_1,locations_2)
np.allclose(old,new)
True
Если мы посмотрим на тайминги:
%timeit new_get_distances(locations_1,locations_2)
10000 loops, best of 3: 80.6 µs per loop
%timeit get_distances(locations_1,locations_2)
10000 loops, best of 3: 74.9 µs per loop
это на самом деле медленнее для небольшого примера; однако давайте посмотрим на более крупный пример:
locations_1 = np.random.rand(1000,2)
locations_2 = np.random.rand(1000,2)
%timeit get_distances(locations_1,locations_2)
1 loops, best of 3: 5.84 s per loop
%timeit new_get_distances(locations_1,locations_2)
10 loops, best of 3: 149 ms per loop
Теперь у нас есть ускорение 40x. Возможно, может выжать еще немного скорости в нескольких местах.
Edit: сделал несколько обновлений, чтобы вырезать избыточные места и дать понять, что мы не изменяем исходные массивы местоположения.
больше efiicient при использовании meshgrid, чтобы заменить двойной цикл for:
import numpy as np
earth_radius = 3958.75
def get_distances(locs_1, locs_2):
lats1, lats2 = np.meshgrid(locs_1[:,0], locs_2[:,0])
lons1, lons2 = np.meshgrid(locs_1[:,1], locs_2[:,1])
lat_dif = np.radians(lats1 - lats2)
long_dif = np.radians(lons1 - lons2)
sin_d_lat = np.sin(lat_dif / 2.)
sin_d_long = np.sin(long_dif / 2.)
step_1 = (sin_d_lat ** 2) + (sin_d_long ** 2) * np.cos(np.radians(lats1[0])) * np.cos(np.radians(lats2[0]))
step_2 = 2 * np.arctan2(np.sqrt(step_1), np.sqrt(1-step_1))
dist = step_2 * earth_radius
return dist
обеспечивает ли формула Haversine достаточную точность для вашего использования? Он может быть выключен совсем немного. Я думаю, вы сможете получить обе точности и скорость, если вы используете proj.4, в частности привязки python,pyproj. Обратите внимание, что pyproj может работать непосредственно на массивах координат numpy.