python numpy pandas gis haversine

Aproximación rápida de Haversine(Python/Pandas)



numpy gis (5)

Cada fila en un marco de datos de Pandas contiene coordenadas lat / lng de 2 puntos. ¡Usar el código de Python a continuación, calcular la distancia entre estos 2 puntos para muchas (millones) de filas lleva mucho tiempo!

Teniendo en cuenta que los 2 puntos están separados por menos de 50 millas y la precisión no es muy importante, ¿es posible hacer el cálculo más rápido?

from math import radians, cos, sin, asin, sqrt def haversine(lon1, lat1, lon2, lat2): """ Calculate the great circle distance between two points on the earth (specified in decimal degrees) """ # convert decimal degrees to radians lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) # haversine formula dlon = lon2 - lon1 dlat = lat2 - lat1 a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 c = 2 * asin(sqrt(a)) km = 6367 * c return km for index, row in df.iterrows(): df.loc[index, ''distance''] = haversine(row[''a_longitude''], row[''a_latitude''], row[''b_longitude''], row[''b_latitude''])


Algunas de estas respuestas son "redondeando" el radio de la tierra. Si los compara con otras calculadoras de distancia (como geopy ), estas funciones estarán desactivadas.

Puede cambiar R=3959.87433 para la constante de conversión a continuación si desea obtener la respuesta en millas.

Si quieres kilómetros, usa R= 6372.8 .

lon1 = -103.548851 lat1 = 32.0004311 lon2 = -103.6041946 lat2 = 33.374939 def haversine(lat1, lon1, lat2, lon2): R = 3959.87433 # this is in miles. For Earth radius in kilometers use 6372.8 km dLat = radians(lat2 - lat1) dLon = radians(lon2 - lon1) lat1 = radians(lat1) lat2 = radians(lat2) a = sin(dLat/2)**2 + cos(lat1)*cos(lat2)*sin(dLon/2)**2 c = 2*asin(sqrt(a)) return R * c print(haversine(lat1, lon1, lat2, lon2))


Aquí hay una versión numérica vectorizada de la misma función:

import numpy as np def haversine_np(lon1, lat1, lon2, lat2): """ Calculate the great circle distance between two points on the earth (specified in decimal degrees) All args must be of equal length. """ 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 = 6367 * c return km

Las entradas son todas matrices de valores, y deberían poder hacer millones de puntos al instante. El requisito es que las entradas sean ndarrays pero las columnas de tu tabla de pandas funcionarán.

Por ejemplo, con valores generados aleatoriamente:

>>> import numpy as np >>> import pandas >>> lon1, lon2, lat1, lat2 = np.random.randn(4, 1000000) >>> df = pandas.DataFrame(data={''lon1'':lon1,''lon2'':lon2,''lat1'':lat1,''lat2'':lat2}) >>> km = haversine_np(df[''lon1''],df[''lat1''],df[''lon2''],df[''lat2''])

El bucle a través de matrices de datos es muy lento en python. Numpy proporciona funciones que operan en arreglos completos de datos, lo que le permite evitar los bucles y mejorar drásticamente el rendimiento.

Este es un ejemplo de vectorization .


En caso de que se permita el uso de scikit-learn, le daría una oportunidad al siguiente:

from sklearn.neighbors import DistanceMetric dist = DistanceMetric.get_metric(''haversine'') # example data lat1, lon1 = 36.4256345, -5.1510261 lat2, lon2 = 40.4165, -3.7026 lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) X = [[lat1, lon1], [lat2, lon2]] kms = 6367 print(kms * dist.pairwise(X))


Simplemente por un ejemplo ilustrativo, tomé la versión numpy en la respuesta de @ballsdotballs y también hice una implementación de C complementaria para ser llamada a través de ctypes . Dado que numpy es una herramienta altamente optimizada, hay pocas posibilidades de que mi código C sea tan eficiente, pero debería ser un tanto cercano. La gran ventaja aquí es que al ejecutar un ejemplo con tipos C, puede ayudarlo a ver cómo puede conectar sus propias funciones personales de C a Python sin demasiada sobrecarga. Esto es especialmente bueno cuando solo quieres optimizar una pequeña parte de un cálculo más grande escribiendo esa pequeña parte en alguna fuente de C en lugar de Python. El simple uso de numpy solucionará el problema la mayor parte del tiempo, pero para aquellos casos en los que realmente no necesita todos los numpy y no desea agregar el acoplamiento para requerir el uso de tipos de datos numpy en algún código, es muy útil para saber cómo bajar a la biblioteca de ctypes y hacerlo usted mismo.

Primero vamos a crear nuestro archivo fuente C, llamado haversine.c :

#include <stdlib.h> #include <stdio.h> #include <math.h> int haversine(size_t n, double *lon1, double *lat1, double *lon2, double *lat2, double *kms){ if ( lon1 == NULL || lon2 == NULL || lat1 == NULL || lat2 == NULL || kms == NULL){ return -1; } double km, dlon, dlat; double iter_lon1, iter_lon2, iter_lat1, iter_lat2; double km_conversion = 2.0 * 6367.0; double degrees2radians = 3.14159/180.0; int i; for(i=0; i < n; i++){ iter_lon1 = lon1[i] * degrees2radians; iter_lat1 = lat1[i] * degrees2radians; iter_lon2 = lon2[i] * degrees2radians; iter_lat2 = lat2[i] * degrees2radians; dlon = iter_lon2 - iter_lon1; dlat = iter_lat2 - iter_lat1; km = pow(sin(dlat/2.0), 2.0) + cos(iter_lat1) * cos(iter_lat2) * pow(sin(dlon/2.0), 2.0); kms[i] = km_conversion * asin(sqrt(km)); } return 0; } // main function for testing int main(void) { double lat1[2] = {16.8, 27.4}; double lon1[2] = {8.44, 1.23}; double lat2[2] = {33.5, 20.07}; double lon2[2] = {14.88, 3.05}; double kms[2] = {0.0, 0.0}; size_t arr_size = 2; int res; res = haversine(arr_size, lon1, lat1, lon2, lat2, kms); printf("%d/n", res); int i; for (i=0; i < arr_size; i++){ printf("%3.3f, ", kms[i]); } printf("/n"); }

Tenga en cuenta que estamos tratando de mantener con las convenciones de C. Pase explícitamente argumentos de datos por referencia, usando size_t para una variable de tamaño, y esperando que nuestra función haversine funcione mutando una de las entradas pasadas de manera que contenga los datos esperados en la salida. La función en realidad devuelve un entero, que es un indicador de éxito / falla que podría ser usado por otros consumidores de nivel C de la función.

Vamos a necesitar encontrar una manera de manejar todos estos pequeños problemas específicos de C dentro de Python.

A continuación, pongamos nuestra versión numpy de la función junto con algunas importaciones y algunos datos de prueba en un archivo llamado haversine.py :

import time import ctypes import numpy as np from math import radians, cos, sin, asin, sqrt def haversine(lon1, lat1, lon2, lat2): """ Calculate the great circle distance between two points on the earth (specified in decimal degrees) """ # convert decimal degrees to radians lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) # haversine formula dlon = lon2 - lon1 dlat = lat2 - lat1 a = (np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2) c = 2 * np.arcsin(np.sqrt(a)) km = 6367 * c return km if __name__ == "__main__": lat1 = 50.0 * np.random.rand(1000000) lon1 = 50.0 * np.random.rand(1000000) lat2 = 50.0 * np.random.rand(1000000) lon2 = 50.0 * np.random.rand(1000000) t0 = time.time() r1 = haversine(lon1, lat1, lon2, lat2) t1 = time.time() print t1-t0, r1

Escogí hacer lats y lons (en grados) que se eligen aleatoriamente entre 0 y 50, pero no importa demasiado para esta explicación.

Lo siguiente que debemos hacer es compilar nuestro módulo C de tal manera que Python pueda cargarlo dinámicamente. Estoy usando un sistema Linux (puedes encontrar ejemplos para otros sistemas muy fácilmente en Google), por lo que mi objetivo es compilar haversine.c en un objeto compartido, como por ejemplo:

gcc -shared -o haversine.so -fPIC haversine.c -lm

También podemos compilar en un ejecutable y ejecutarlo para ver qué funciones main del programa C muestra:

> gcc haversine.c -o haversine -lm > ./haversine 0 1964.322, 835.278,

Ahora que hemos compilado el objeto compartido haversine.so , podemos usar ctypes para cargarlo en Python y necesitamos proporcionar la ruta al archivo para hacerlo:

lib_path = "/path/to/haversine.so" # Obviously use your real path here. haversine_lib = ctypes.CDLL(lib_path)

Ahora haversine_lib.haversine actúa casi como una función de Python, excepto que es posible que tengamos que hacer algunos cálculos de tipo manual para asegurarnos de que las entradas y salidas se interpreten correctamente.

numpy realidad proporciona algunas herramientas útiles para esto y la que numpy.ctypeslib aquí es numpy.ctypeslib . Vamos a construir un tipo de puntero que nos permitirá pasar numpy.ndarrays a estas funciones ctypes ctypes, ya que a través de ellos eran punteros. Aquí está el código:

arr_1d_double = np.ctypeslib.ndpointer(dtype=np.double, ndim=1, flags=''CONTIGUOUS'') haversine_lib.haversine.restype = ctypes.c_int haversine_lib.haversine.argtypes = [ctypes.c_size_t, arr_1d_double, arr_1d_double, arr_1d_double, arr_1d_double, arr_1d_double]

Observe que le decimos a la función haversine_lib.haversine que interprete sus argumentos de acuerdo con los tipos que deseamos.

Ahora, para probarlo desde Python, lo que queda es simplemente hacer una variable de tamaño, y una matriz que será mutada (como en el código C) para contener los datos de resultados, entonces podemos llamarlo:

size = len(lat1) output = np.empty(size, dtype=np.double) print "=====" print output t2 = time.time() res = haversine_lib.haversine(size, lon1, lat1, lon2, lat2, output) t3 = time.time() print t3 - t2, res print type(output), output

Poniéndolo todo junto en el bloque __main__ de haversine.py , el archivo completo ahora se ve así:

import time import ctypes import numpy as np from math import radians, cos, sin, asin, sqrt def haversine(lon1, lat1, lon2, lat2): """ Calculate the great circle distance between two points on the earth (specified in decimal degrees) """ # convert decimal degrees to radians lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) # haversine formula dlon = lon2 - lon1 dlat = lat2 - lat1 a = (np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2) c = 2 * np.arcsin(np.sqrt(a)) km = 6367 * c return km if __name__ == "__main__": lat1 = 50.0 * np.random.rand(1000000) lon1 = 50.0 * np.random.rand(1000000) lat2 = 50.0 * np.random.rand(1000000) lon2 = 50.0 * np.random.rand(1000000) t0 = time.time() r1 = haversine(lon1, lat1, lon2, lat2) t1 = time.time() print t1-t0, r1 lib_path = "/home/ely/programming/python/numpy_ctypes/haversine.so" haversine_lib = ctypes.CDLL(lib_path) arr_1d_double = np.ctypeslib.ndpointer(dtype=np.double, ndim=1, flags=''CONTIGUOUS'') haversine_lib.haversine.restype = ctypes.c_int haversine_lib.haversine.argtypes = [ctypes.c_size_t, arr_1d_double, arr_1d_double, arr_1d_double, arr_1d_double, arr_1d_double] size = len(lat1) output = np.empty(size, dtype=np.double) print "=====" print output t2 = time.time() res = haversine_lib.haversine(size, lon1, lat1, lon2, lat2, output) t3 = time.time() print t3 - t2, res print type(output), output

Para ejecutarlo, que ejecutará y ctypes versiones de Python y ctypes separado e imprimirá algunos resultados, solo podemos hacer

python haversine.py

que muestra:

0.111340045929 [ 231.53695005 3042.84915093 169.5158946 ..., 1359.2656769 2686.87895954 3728.54788207] ===== [ 6.92017600e-310 2.97780954e-316 2.97780954e-316 ..., 3.20676686e-001 1.31978329e-001 5.15819721e-001] 0.148446083069 0 <type ''numpy.ndarray''> [ 231.53675618 3042.84723579 169.51575588 ..., 1359.26453029 2686.87709456 3728.54493339]

Como se esperaba, la versión numpy es ligeramente más rápida (0.11 segundos para vectores con una longitud de 1 millón), pero nuestra versión rápida y sucia de ctypes no se queda atrás: un respetable 0.148 segundos con los mismos datos.

Comparemos esto con una solución ingenua de bucle for en Python:

from math import radians, cos, sin, asin, sqrt def slow_haversine(lon1, lat1, lon2, lat2): n = len(lon1) kms = np.empty(n, dtype=np.double) for i in range(n): lon1_v, lat1_v, lon2_v, lat2_v = map( radians, [lon1[i], lat1[i], lon2[i], lat2[i]] ) dlon = lon2_v - lon1_v dlat = lat2_v - lat1_v a = (sin(dlat/2)**2 + cos(lat1_v) * cos(lat2_v) * sin(dlon/2)**2) c = 2 * asin(sqrt(a)) kms[i] = 6367 * c return kms

Cuando coloco esto en el mismo archivo de Python que los otros y cronometro en los mismos datos de millones de elementos, constantemente veo un tiempo de alrededor de 2.65 segundos en mi máquina.

Entonces, al cambiar rápidamente a ctypes , mejoramos la velocidad en un factor de aproximadamente 18. Para muchos cálculos que pueden beneficiarse del acceso a datos simples y contiguos, a menudo se ven ganancias mucho más altas incluso que esto.

Solo para ser súper claro, no estoy respaldando esto como una mejor opción que solo usar numpy . Este es precisamente el problema por el que se construyó el numpy para resolverlo, por lo que ctypes tu propio código ctypes cada vez que sea (a) tiene sentido incorporar tipos de datos numpy en tu aplicación y (b) existe una manera fácil de asignar tu código a un numpy equivalente numpy , no es muy eficiente.

Pero sigue siendo muy útil saber cómo hacer esto para aquellas ocasiones en las que prefiera escribir algo en C y llamarlo en Python, o en situaciones en numpy que no sea práctico depender de numpy (en un sistema integrado donde no se puede instalar numpy , por numpy ejemplo).


Una extensión trivial de la solución vectorizada de @ derricw , puede usar numba para mejorar el rendimiento en ~ 2x prácticamente sin cambios en su código. Para cálculos numéricos puros, esto probablemente debería usarse para pruebas comparativas / pruebas frente a soluciones posiblemente más eficientes.

from numba import njit @njit def haversine_nb(lon1, lat1, lon2, lat2): lon1, lat1, lon2, lat2 = np.radians(lon1), np.radians(lat1), np.radians(lon2), np.radians(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 return 6367 * 2 * np.arcsin(np.sqrt(a))

Benchmarking versus la función de Pandas:

%timeit haversine_pd(df[''lon1''], df[''lat1''], df[''lon2''], df[''lat2'']) # 1 loop, best of 3: 1.81 s per loop %timeit haversine_nb(df[''lon1''].values, df[''lat1''].values, df[''lon2''].values, df[''lat2''].values) # 1 loop, best of 3: 921 ms per loop

Código completo de referencia:

import pandas as pd, numpy as np from numba import njit def haversine_pd(lon1, lat1, lon2, lat2): 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 return 6367 * 2 * np.arcsin(np.sqrt(a)) @njit def haversine_nb(lon1, lat1, lon2, lat2): lon1, lat1, lon2, lat2 = np.radians(lon1), np.radians(lat1), np.radians(lon2), np.radians(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 return 6367 * 2 * np.arcsin(np.sqrt(a)) np.random.seed(0) lon1, lon2, lat1, lat2 = np.random.randn(4, 10**7) df = pd.DataFrame(data={''lon1'':lon1,''lon2'':lon2,''lat1'':lat1,''lat2'':lat2}) km = haversine_pd(df[''lon1''], df[''lat1''], df[''lon2''], df[''lat2'']) km_nb = haversine_nb(df[''lon1''].values, df[''lat1''].values, df[''lon2''].values, df[''lat2''].values) assert np.isclose(km.values, km_nb).all() %timeit haversine_pd(df[''lon1''], df[''lat1''], df[''lon2''], df[''lat2'']) # 1 loop, best of 3: 1.81 s per loop %timeit haversine_nb(df[''lon1''].values, df[''lat1''].values, df[''lon2''].values, df[''lat2''].values) # 1 loop, best of 3: 921 ms per loop