Python быстро вычисляет много расстояний

У меня есть ввод 36 742 точки, что означает, что если бы я хотел вычислить нижний треугольник матрицы расстояний (используя приближение Винсенти), мне нужно было бы сгенерировать 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 км), а затем рассчитайте только точные расстояния между этими магазинами.
  • Вместо хранения точек в списке используйте что-то вроде дерева квадрантов, но я думаю, что это помогает только для ранжирования близких точек, а не фактического расстояния -> поэтому я предполагаю, что это какая-то база геоданных
  • Очевидно, я могу попробовать гаверсинус или проект и использовать евклидовы расстояния, однако я заинтересован в использовании максимально точной меры.
  • Используйте параллельную обработку (однако у меня возникли некоторые трудности с тем, как сократить список, чтобы получить все соответствующие пары).

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

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. Я пытался использовать лямбда и карту:

# [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 секунды (я ограничил количество магазинов до 2000 с 32 000). Возможно, я неправильно использовал карту?


person mptevsion    schedule 09.02.2016    source источник
comment
Все это звучит как хорошие идеи... в чем вопрос?   -  person Emma    schedule 09.02.2016
comment
С таким количеством точек мне трудно выбрать лучший способ действий, и я надеялся получить некоторые указания о том, что попробовать и на что не тратить свое время.   -  person mptevsion    schedule 09.02.2016
comment
@Эмма Ой да ладно! Вопрос кристально ясен.   -  person Pouria    schedule 09.02.2016
comment
Если у вас нет мощной машины, вам, возможно, придется делать это частями. Для хранения этих 1,3 миллиарда расстояний потребуется более 10,5 Гбайт памяти.   -  person RootTwo    schedule 10.02.2016


Ответы (4)


Это звучит как классический пример использования k-D деревьев. .

Если вы сначала преобразуете свои точки в евклидово пространство, вы можете использовать метод query_pairs из scipy.spatial.cKDTree:

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 будет set из (i, j) кортежей, соответствующих индексам строк пар магазинов, которые находятся на расстоянии ≤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
person ali_m    schedule 09.02.2016
comment
Ах, спасибо! Похоже, что это аналогичный подход к использованию геохеширования — вы случайно не знаете adv/disad. по сравнению с ним? - person mptevsion; 09.02.2016
comment
TBH Я никогда раньше не сталкивался с геохешированием (я нейробиолог, а не географ!). На первый взгляд, модуль geoindex, на который вы ссылаетесь, кажется, реализован на чистом Python и требует циклического перебора элементов массива в Python, тогда как cKDTree написан на C и напрямую работает с массивами numpy. Помимо алгоритмической эффективности, я мог бы ожидать, что cKDTree будет быстрее благодаря гораздо меньшим накладным расходам Python, но я действительно недостаточно знаю об алгоритме геохеширования, чтобы дать вам правильный ответ. Я предлагаю вам сравнить их и узнать! - person ali_m; 09.02.2016
comment
Али, есть ли у вас какие-либо предложения о том, как сохранить матрицу разреженных расстояний ckdtree в виде уникальных парных комбинаций? Вместо матрицы - person mptevsion; 10.02.2016
comment
Еще раз спасибо! Я также использовал np.column_stack, чтобы экспортировать все это в CSV: w.writerows(np.column_stack((points[udist.row],points[udist.col], udist.data))) - person mptevsion; 10.02.2016
comment
CSV не лучший выбор выходного формата. Текстовые форматы требуют больше места для хранения и, как правило, намного медленнее и требуют больше памяти для чтения и записи, чем двоичные форматы (например, собственный формат numpy .npy). В случае данных с плавающей запятой они также гарантируют некоторую потерю точности, поскольку невозможно точно представить произвольное число с плавающей запятой с помощью десятичной строки конечной длины. Текстовые форматы имеют наибольший смысл в тех случаях, когда данные должны быть удобочитаемыми для человека, но я предполагаю, что большинство людей никогда не захотят читать более миллиона строк значений с плавающей запятой... - person ali_m; 10.02.2016
comment
Али, я только что заметил странную проблему; есть некоторые наблюдения, которые находятся в пределах 500 метров (например), однако, если я устанавливаю отсечку в 1600 метров, они сбрасываются. Я пробовал запускать все это только на них (например, 5 баллов), и это работает, поэтому мне интересно, есть ли что-то не так в коде, который их сбрасывает? Если это поможет, мой последний «ответ» содержит мой полный код. Я не слишком уверен, почему это происходит - не то чтобы 500 метров ближе к пограничному случаю (1600) - person mptevsion; 11.02.2016
comment
Просто добавлю, что порядок моего входного CSV влияет на конечное количество очков, которые я получаю в пределах 1600 метров? Вот я и подумал, может быть, эта линия влияет на него? udist = sparse.tril(tree_dist, k=-1) - person mptevsion; 11.02.2016

Пробовали ли вы отображать целые массивы и функции вместо того, чтобы перебирать их? Пример будет следующим:

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 ms.

Твой выбор!

person Pouria    schedule 09.02.2016
comment
Спасибо за это! Я попытался реализовать это в своем коде (см. Edit2 к моему исходному сообщению), однако не нашел ускорения. Для 2000 магазинов исходный код занял 61,3 секунды, а этот — 61,1 секунды. - person mptevsion; 09.02.2016
comment
Вы замеряли это так: %timeit out_dist = list(map(func,lwr_tr_mat)) или замеряли весь процесс? Потому что у вас все еще есть итерации в первой строке. - person Pouria; 09.02.2016
comment
Я использовал: start = time.clock() out_dist = list(map(func_cond,lwr_tr_mat)) print(time.clock() - start) - person mptevsion; 09.02.2016
comment
ааа. Вот в чем причина. Время тратится на эту итерацию, а не на отображение. Измените свою матрицу на n на 1 или применяйте каждую итерацию как функцию, сопоставленную с матрицей/массивом. - person Pouria; 09.02.2016
comment
Также установите jupyter или ipython и функцию %timeit для проверки отдельных линий. Это точнее и информативнее. - person Pouria; 09.02.2016
comment
Модуль timeit доставлял мне несколько проблем ... дольше, чем самый быстрый. Это может означать, что промежуточный результат кэшируется... поэтому вместо этого я просто использовал time(). Однако я не уверен, что вы подразумеваете под применением каждой итерации? Вы имеете в виду, что вместо использования генератора загружаете все в оперативную память? - person mptevsion; 09.02.2016
comment
%timeit пропускает строку через цикл (выбирает по умолчанию наилучшее количество итераций от 1 до 10k) и берет среднее значение. Это более точно, потому что (1) таким образом исключается время, необходимое для вызова функции, и (2) исключаются изменения времени, которые могут возникнуть из-за других небольших операций, которые могут выполняться в системе. - person Pouria; 09.02.2016
comment
Ага. Не используйте итерации, иначе вы вернетесь к исходной точке. Либо векторизация, либо отображение. Забудьте о существовании итераций. Если вы обнаружите, что не можете этого сделать, напишите код C/Cython для выполнения этой части. - person Pouria; 09.02.2016
comment
Хм, я думаю, что создание списка N * (N-1) * 0,5 для N = 35 000 само по себе заняло бы несколько часов. - person mptevsion; 09.02.2016
comment
Нет, если вы делаете это путем отображения или использования numpy.reshape, тогда вы можете использовать функцию var.tolist() для получения list(), при условии, что var является объектом numpy.array(). - person Pouria; 09.02.2016
comment
Пурия, не могли бы вы перечислить несколько функций numpy, которые я могу найти в Google, чтобы получить это? Из базы np.genfromtxt(path_to_csv, dtype=float, delimiter=',', names=True) , где я бы их пересек, чтобы получить длинный список нижнего треугольника - person mptevsion; 09.02.2016
comment
Вы найдете reshape [ docs.scipy.org/doc/numpy-1.10.1/reference/generated/] и сверните [ docs.scipy.org/doc/numpy-1.10.1/reference/generated/ ]. - person Pouria; 09.02.2016

Спасибо всем за помощь. Я думаю, что решил это, включив все предложения.

Я использую numpy для импорта географических координат, а затем проецирую их, используя «France Lambert - 93». Это позволяет мне заполнить scipy.spatial.cKDTree точками, а затем рассчитать матрицу sparse_distance_matrix, указав отсечку в 50 км (мои прогнозируемые точки указаны в метрах). Затем я извлекаю нижний треугольник в 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 секунд на сохранение на диск.

Затем я сравнил разницу в расстоянии Винсента и расстоянии гипотенузы (в проецируемых координатах).

Средняя разница в метрах составила 2,7, а средняя разница в метрах — 0,0073 %, что выглядит великолепно.

person mptevsion    schedule 10.02.2016

«Используйте какое-то хеширование, чтобы получить быстрый грубый разрез (все магазины в пределах 100 км), а затем рассчитайте только точные расстояния между этими магазинами». Я думаю, что это лучше назвать гриддингом. Итак, сначала сделайте диктовку с набором координат в качестве ключа и поместите каждый магазин в 50-километровое ведро рядом с этой точкой. тогда, когда вы вычисляете расстояния, вы смотрите только на близлежащие ведра, а не перебираете каждый магазин во всей вселенной.

person bmbigbang    schedule 09.02.2016