Python: ускорение географического сравнения

Я написал код, который включает вложенный цикл, где внутренний цикл выполняется примерно 1,5 миллиона раз. У меня есть функция в этом цикле, которую я пытаюсь оптимизировать. Я проделал определенную работу и получил некоторые результаты, но мне нужен небольшой вклад, чтобы проверить, насколько я уверен, что я делаю.

Некоторая предыстория:

У меня есть две коллекции географических точек (широта, долгота), одна относительно небольшая коллекция и одна относительно огромная коллекция. Для каждой точки в небольшой коллекции мне нужно найти ближайшую точку в большой коллекции.

Очевидным способом сделать это будет использование формулы haversine. Преимущество здесь в том, что расстояния определенно точны.

from math import radians, sin, cos, asin, sqrt

def haversine(point1, point2):
    """Gives the distance between two points on earth.
    """
    earth_radius_miles = 3956
    lat1, lon1 = (radians(coord) for coord in point1)
    lat2, lon2 = (radians(coord) for coord in point2)
    dlat, dlon = (lat2 - lat1, lon2 - lon1)
    a = sin(dlat/2.0)**2 + cos(lat1) * cos(lat2) * sin(dlon/2.0)**2
    great_circle_distance = 2 * asin(min(1,sqrt(a)))
    d = earth_radius_miles * great_circle_distance
    return d

Однако запуск этого 1,5 миллиона раз занимает около 9 секунд на моей машине (в соответствии с timeit). Поскольку точное расстояние не имеет значения, скорее мне нужно всего лишь найти ближайшую точку, я решил попробовать другие функции.

Простая реализация теоремы о пифагоре дает мне скорость около 30%. Думая, что я могу сделать лучше, я написал следующее:

def dumb(point1, point2):
    lat1, lon1 = point1
    lat2, lon2 = point2
    d = abs((lat2 - lat1) + (lon2 - lon1))

что дает мне улучшение в 10 раз. Однако теперь я беспокоюсь, что это не сохранит неравенство треугольника.

Итак, мой последний вопрос в два раза: я хотел бы иметь функцию, которая работает так же быстро, как dumb, но все равно будет правильной. Будет ли работать dumb? Если нет, какие-либо предложения о том, как улучшить мою функцию haversine?

Ответ 1

Вы можете рассмотреть какое-то графическое хеширование, т.е. быстро найти ближайшие точки, а затем вычислить их. Например, вы можете создать единую сетку и распределить точки (большой коллекции) в бункерах, созданных сеткой.

Теперь, имея точку из небольшой коллекции, вам нужно обработать гораздо меньшее количество очков (т.е. только в соответствующих ячейках)

Ответ 2

Это тот расчет, который numpy действительно хорош. Вместо того, чтобы перебирать весь большой набор координат, вы можете вычислить расстояние между одной точкой и всем набором данных за один расчет. В моих тестах ниже вы можете получить прирост скорости на порядок.

Вот некоторые тесты времени с вашим методом haversine, ваш метод dumb (не совсем уверен, что это делает) и мой метод numpy haversine. Он вычисляет расстояние между двумя точками - одно в Вирджинии и одно в Калифорнии, которое находится в 2293 милях от.

from math import radians, sin, cos, asin, sqrt, pi, atan2
import numpy as np
import itertools

earth_radius_miles = 3956.0

def haversine(point1, point2):
    """Gives the distance between two points on earth.
    """
    lat1, lon1 = (radians(coord) for coord in point1)
    lat2, lon2 = (radians(coord) for coord in point2)
    dlat, dlon = (lat2 - lat1, lon2 - lon1)
    a = sin(dlat/2.0)**2 + cos(lat1) * cos(lat2) * sin(dlon/2.0)**2
    great_circle_distance = 2 * asin(min(1,sqrt(a)))
    d = earth_radius_miles * great_circle_distance
    return d

def dumb(point1, point2):
    lat1, lon1 = point1
    lat2, lon2 = point2
    d = abs((lat2 - lat1) + (lon2 - lon1))
    return d

def get_shortest_in(needle, haystack):
    """needle is a single (lat,long) tuple.
        haystack is a numpy array to find the point in
        that has the shortest distance to needle
    """
    dlat = np.radians(haystack[:,0]) - radians(needle[0])
    dlon = np.radians(haystack[:,1]) - radians(needle[1])
    a = np.square(np.sin(dlat/2.0)) + cos(radians(needle[0])) * np.cos(np.radians(haystack[:,0])) * np.square(np.sin(dlon/2.0))
    great_circle_distance = 2 * np.arcsin(np.minimum(np.sqrt(a), np.repeat(1, len(a))))
    d = earth_radius_miles * great_circle_distance
    return np.min(d)


x = (37.160316546736745, -78.75)
y = (39.095962936305476, -121.2890625)

def dohaversine():
    for i in xrange(100000):
        haversine(x,y)

def dodumb():
    for i in xrange(100000):
        dumb(x,y)

lots = np.array(list(itertools.repeat(y, 100000)))
def donumpy():
    get_shortest_in(x, lots)

from timeit import Timer
print 'haversine distance =', haversine(x,y), 'time =',
print Timer("dohaversine()", "from __main__ import dohaversine").timeit(100)
print 'dumb distance =', dumb(x,y), 'time =',
print Timer("dodumb()", "from __main__ import dodumb").timeit(100)
print 'numpy distance =', get_shortest_in(x, lots), 'time =',
print Timer("donumpy()", "from __main__ import donumpy").timeit(100)

И вот что он печатает:

haversine distance = 2293.13242188 time = 44.2363960743
dumb distance = 40.6034161104 time = 5.58199882507
numpy distance = 2293.13242188 time = 1.54996609688

Метод numpy занимает 1,55 секунды для вычисления того же количества вычислений расстояний, поскольку для вычисления с помощью метода функции требуется 44.24 секунд. Вероятно, вы можете получить больше ускорения, объединив некоторые функции numpy в один оператор, но это станет длинной, трудно читаемой строкой.

Ответ 3

Формула, которую вы написали (d = abs (lat2-lat1) + (lon2-lon1)), НЕ сохраняет неравенство треугольника: если вы найдете lat, lon для w d min, вы не найдете ближайшую точку, но точка, ближайшая к двум диагональным линиям, пересекающимся в точке, которую вы проверяете!

Я думаю, вы должны заказать большое количество точек лат и лон (это означает: (1,1), (1,2), (1,3)... (2,1), (2, 2) и т.д. Затем используйте метод наводчика, чтобы найти некоторые из ближайших точек в терминах широты и долготы (это должно быть очень быстро, оно будет принимать время процессора, пропорциональное ln2 (n), где n - количество точек). Вы можете сделать это легко, например: выберите все точки в квадрате 10x10 вокруг точки, которую вы собираетесь проверять, это означает: найдите все точки, которые находятся от -10 до +10 в lat (метод наводчика) и снова те, которые находятся от -10 до +10 в лондонском (метод наводчика). Теперь у вас действительно небольшой объем данных, и он должен быть очень быстрым!

Ответ 4

abs(lat2 - lat1) + abs(lon2 - lon1) является 1-нормой или манхеттен-метрикой и, следовательно, имеет место неравенство треугольника.

Ответ 5

У меня была аналогичная проблема и я решил сбить функцию Cython. На моем MBB 2008 года он может выполнять около 1,2 М итераций в секунду. При проверке типа скорость увеличивается еще на 25%. Без сомнения, возможны дальнейшие оптимизации (за счет ясности).

Вы также можете проверить функцию scipy.spatial.distance.cdist.

from libc.math cimport sin, cos, acos

def distance(float lat1, float lng1, float lat2, float lng2):
    if lat1 is None or lat2 is None or lng1 is None or lng2 is None: return None
    cdef float phi1
    cdef float phi2
    cdef float theta1
    cdef float theta2
    cdef float c
    cdef float arc

    phi1 = (90.0 - lat1)*0.0174532925
    phi2 = (90.0 - lat2)*0.0174532925
    theta1 = lng1*0.0174532925
    theta2 = lng2*0.0174532925

    c = (sin(phi1)*sin(phi2)*cos(theta1 - theta2) + cos(phi1)*cos(phi2))
    arc = acos( c )
    return arc*6371

Ответ 6

Самый быстрый способ сделать это - избегать вычисления функции для каждой пары точек, предполагая, что ваша относительно небольшая коллекция не очень мала.

Существуют базы данных, в которых есть геоиндексы, которые вы могли бы использовать (mysql, oracle, mongodb..) или реализовать что-то самостоятельно.

Вы можете использовать python-geohash. Для каждого документа в более мелкой коллекции вам нужно быстро найти набор документов в большой коллекции, которые разделяют хэш из geohash.neighbors для самого длинного размера хэша, который имеет совпадения. Вам понадобится использовать соответствующую структуру данных для поиска, или это будет медленным.

Для нахождения расстояния между точками погрешность простого подхода возрастает по мере увеличения расстояния между точками, а также зависит от широты. См. http://www.movable-type.co.uk/scripts/gis-faq-5.1.html, например.