Chaque ligne d’un cadre de données Pandas contient les coordonnées lat/lng de 2 points. En utilisant le code Python ci-dessous, le calcul des distances entre ces 2 points pour plusieurs (millions) de lignes prend beaucoup de temps!
Étant donné que les 2 points sont séparés par moins de 50 milles et que la précision n’est pas très importante, est-il possible d’accélérer le calcul?
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'])
Voici une version numpy vectorisée de la même fonction:
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
Les entrées sont toutes des tableaux de valeurs, et il devrait pouvoir faire des millions de points instantanément. L’exigence est que les entrées soient ndarrays mais les colonnes de votre table de pandas fonctionneront.
Par exemple, avec des valeurs générées aléatoirement:
>>> 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'])
La lecture en boucle dans des tableaux de données est très lente en python. Numpy fournit des fonctions qui fonctionnent sur des tableaux de données entiers, ce qui vous permet d'éviter les boucles et d'améliorer considérablement les performances.
Ceci est un exemple de vectorisation .
Pour illustrer mon propos, j'ai pris la version de numpy
dans la réponse de @ballsdotballs et j'ai également créé une implémentation C complémentaire qui doit être appelée via ctypes
. Puisque numpy
est un outil hautement optimisé, mon code C a peu de chances d'être aussi efficace, mais il devrait être un peu proche. Le gros avantage ici est que, en parcourant un exemple avec des types C, vous pouvez voir comment vous pouvez connecter vos propres fonctions C personnelles à Python sans trop de frais supplémentaires. C’est particulièrement agréable lorsque vous souhaitez simplement optimiser un petit morceau d’un calcul plus volumineux en écrivant ce petit morceau dans une source C plutôt que dans Python. Le simple fait d'utiliser numpy
résoudra le problème la plupart du temps, mais dans les cas où vous n'avez pas vraiment besoin de tous les éléments de numpy
et que vous ne souhaitez pas ajouter le couplage pour nécessiter l'utilisation de numpy
types de données dans certains codes, il est très pratique de savoir comment accéder à la bibliothèque intégrée ctypes
et le faire vous-même.
Commençons par créer notre fichier source C, appelé 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");
}
Notez que nous essayons de rester avec les conventions C. Passage explicite d'arguments de données par référence, en utilisant size_t
pour une variable de taille et en espérant que notre fonction haversine
fonctionnera en mutant l'une des entrées passées de sorte qu'elle contienne les données attendues à la sortie. La fonction renvoie en fait un entier, qui est un indicateur de réussite/échec pouvant être utilisé par d'autres consommateurs de niveau C de la fonction.
Nous allons devoir trouver un moyen de gérer tous ces petits problèmes spécifiques à C dans Python.
Ensuite, mettons notre version numpy
de la fonction avec quelques importations et quelques données de test dans un fichier appelé 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 __== "__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
J'ai choisi de faire des lats et des lons (en degrés) choisis aléatoirement entre 0 et 50, mais peu importe cette explication.
La prochaine chose que nous devons faire est de compiler notre module C de manière à ce qu’il puisse être chargé dynamiquement par Python. J'utilise un système Linux (vous pouvez trouver très facilement des exemples pour d'autres systèmes sur Google). Mon objectif est donc de compiler haversine.c
dans un objet partagé, comme suit:
gcc -shared -o haversine.so -fPIC haversine.c -lm
Nous pouvons également compiler un fichier exécutable et l'exécuter pour voir ce que la fonction main
du programme C affiche:
> gcc haversine.c -o haversine -lm
> ./haversine
0
1964.322, 835.278,
Maintenant que nous avons compilé l'objet partagé haversine.so
, nous pouvons utiliser ctypes
pour le charger en Python et nous devons fournir le chemin d'accès au fichier pour le faire:
lib_path = "/path/to/haversine.so" # Obviously use your real path here.
haversine_lib = ctypes.CDLL(lib_path)
Désormais, haversine_lib.haversine
se comporte plutôt comme une fonction Python, sauf que nous aurons peut-être besoin d'effectuer un marshaling de type manuel pour nous assurer que les entrées et les sorties sont interprétées correctement.
numpy
fournit en fait des outils utiles pour cela et celui que je vais utiliser ici est numpy.ctypeslib
. Nous allons construire un type de type pointeur qui nous permettra de transmettre numpy.ndarrays
à ces fonctions ctypes
- chargées comme si elles étaient des pointeurs. Voici le code:
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]
Notez que nous demandons au proxy de fonction haversine_lib.haversine
d'interpréter ses arguments en fonction des types souhaités.
Maintenant, pour le tester depuis Python, il ne reste plus qu’à créer une variable de taille et un tableau qui sera muté (comme dans le code C) pour contenir les données de résultat, nous pouvons l’appeler:
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
En regroupant le tout dans le bloc __main__
de haversine.py
, l'ensemble du fichier se présente désormais comme suit:
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 __== "__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
Pour l'exécuter, qui exécutera et chronométrera séparément les versions Python et ctypes
et imprimera quelques résultats, nous pouvons simplement faire
python haversine.py
qui affiche:
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]
Comme prévu, la version numpy
est légèrement plus rapide (0,11 seconde pour les vecteurs d'une longueur de 1 million) mais notre version rapide et sale ctypes
n'est pas en reste: un respectable de 0,148 seconde sur les mêmes données.
Comparons ceci à une solution naïve de boucles 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
Lorsque je mets cela dans le même fichier Python que les autres et que je le fais sur le même million de données, je vois constamment un temps d'environ 2,65 secondes sur ma machine.
Ainsi, en passant rapidement à ctypes
, nous améliorons la vitesse d'un facteur d'environ 18. Pour de nombreux calculs qui peuvent bénéficier d'un accès à des données nues et contiguës, vous constaterez souvent des gains beaucoup plus élevés que cela.
Pour être tout à fait clair, je n’approuve pas du tout que cela soit une meilleure option que d’utiliser numpy
. C’est précisément le problème que numpy
a été conçu pour résoudre le problème. Il est donc logique de créer votre propre code ctypes
chaque fois qu’il est logique (a) d’incorporer les types de données numpy
dans votre application et ) il existe un moyen simple de mapper votre code dans un équivalent numpy
, n’est pas très efficace.
Mais il est toujours très utile de savoir comment faire cela pour les occasions où vous préférez écrire quelque chose en C et l'appelez-le en Python, ou dans les situations où une dépendance à numpy
n'est pas pratique (dans un système embarqué où numpy
ne peut pas être installé, par exemple).
Dans le cas où l'utilisation de scikit-learn est autorisée, je donnerais la chance suivante:
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))
Une extension triviale de la solution vectorisée de @ derricw , vous pouvez utiliser numba
pour améliorer les performances de ~ 2x sans pratiquement modifier votre code. Pour les calculs numériques purs, cela devrait probablement être utilisé pour des tests comparatifs/des tests par rapport à des solutions éventuellement plus efficaces.
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))
Analyse comparative par rapport à la fonction 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
Code de benchmarking complet:
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
Certaines de ces réponses "arrondissent" le rayon de la terre. Si vous les comparez à d'autres calculateurs de distance (tels que geopy ), ces fonctions seront désactivées.
Vous pouvez désactiver R=3959.87433
pour la constante de conversion ci-dessous si vous souhaitez obtenir la réponse en miles.
Si vous voulez des kilomètres, utilisez 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))