快速的Haversine近似算法(Python/Pandas)

58

在Pandas数据框中,每一行包含两个点的纬度/经度坐标。使用以下Python代码计算这两个点之间的距离,在许多(数百万)行上需要很长时间!考虑到这两个点之间的距离不超过50英里且精度并不是非常重要,有可能使计算更快吗?

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'])

1
可能可以避免为大多数候选者计算。计算距离起点50英里的最小和最大经度和纬度。然后使用这些最小值和最大值来淘汰大部分候选者。 - Steven Rumbalski
2
你也可以考虑使用k-d树来构建数据,而不是像DataFrame一样存储在关系结构中。然后,获取给定点的邻居将会很便宜,也许你只需要按需计算距离。应用程序是否总是需要每一对呢?另一个选择可能是对点进行聚类,并使用每个聚类的质心/平均值作为代理。然后,任意两点之间的距离将由仅聚类中心之间的距离近似。虽然这种花哨的方法是否真的比暴力更好还有待商榷。 - ely
1
@Nyxynyx,你在问题中提供的函数计算了大圆距离。而你在评论中的计算则是欧几里得距离。由于地球半径非常大,对于小距离,你可以完全使用欧几里得版本进行近似计算。 - derricw
2
是的,欧几里得逼近法对于足够小的距离可以很好地工作。甚至不需要进行apply操作,可以直接使用数据框中的列。 - reptilicus
Iterrows比纯向量化慢上百到千倍。我已经尝试了13种处理Pandas数据框的方法,希望很快能够展示出来,并绘制出每种方法的速度。 - undefined
显示剩余9条评论
7个回答

146
这是同一个函数的numpy向量化版本:
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 = 6378.137 * c
    return km

输入都是值的数组,应该能够立即处理数百万个点。要求是输入是ndarrays,但是您的pandas表的列也可以使用。
例如,使用随机生成的值:
>>> 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'])

或者如果你想创建另一列:
>>> df['distance'] = haversine_np(df['lon1'],df['lat1'],df['lon2'],df['lat2'])

在Python中,遍历数据数组非常慢。Numpy提供了可以操作整个数据数组的函数,这样可以避免循环,并大大提高性能。
这是向量化的一个示例。

1
很高兴了解到“数组编程”这个术语,之前在MATLAB中没有遇到过。 - Divakar
非常感谢您。小建议:请添加一个真实世界的示例,包括实际坐标,而不是随机值,以澄清输入格式。 - Dennis Golomazov
1
另一个小建议:您可能希望交换函数参数的顺序为 lat,lon。在许多来源中,纬度首先出现,例如在 https://en.wikipedia.org/wiki/Horizontal_position_representation。 - Dennis Golomazov
1
我向sklearn提出了一个功能请求,希望能够添加你的代码:https://github.com/scikit-learn/scikit-learn/issues/17212 - Zach
我刚刚添加了一个Python/Pandas的答案和图表,其中展示了13种迭代Pandas DataFrame的技巧,并且表明手动迭代比纯向量化慢了多达1400倍。顺便说一句,这个答案展示了Haversine技术,我最近引用了它。 - undefined
显示剩余6条评论

20

仅作举例说明,我采用了@ballsdotballs的答案中的numpy版本,并制作了一个伴随的C实现,可通过ctypes调用。由于numpy是一个高度优化的工具,我的C代码很难像它一样高效,但应该会接近一些。这里的重要优势在于,通过使用C类型的示例,可以帮助您了解如何将自己的个人C函数连接到Python,而不需要太多的开销。当您只想通过将某些C源码中的小片段编写成C来优化大型计算中的小部分时,这特别有用。大多数情况下,简单地使用numpy就能解决问题,但对于那些不真正需要全部numpy并且不想添加耦合以要求在整个代码中使用numpy数据类型的情况,知道如何降级到内置的ctypes库并自己完成任务非常方便。

首先让我们创建我们的C源文件,称为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");
}
请注意,我们正在努力遵循C语言的惯例。明确通过引用传递数据参数,使用size_t作为大小变量,并期望我们的haversine函数通过改变其中一个传递的输入来工作,以便在退出时它将包含预期的数据。该函数实际上返回一个整数,这是一个成功/失败标志,可以被其他C级别的函数调用者使用。
我们需要找到一种方法来处理Python中所有这些小的C特定问题。
接下来让我们把我们的numpy版本的函数连同一些导入和一些测试数据放到一个名为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

我选择让经度和纬度(以度为单位)在0到50之间随机选择,但对于本说明并不太重要。

接下来我们需要做的是编译我们的C模块,使得它可以被Python动态加载。我使用的是Linux系统(你可以很容易地在Google上找到其他系统的例子),所以我的目标是将haversine.c编译成共享对象,如下:

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

我们还可以编译成可执行文件并运行它,以查看C程序的main函数显示的内容:

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

现在我们已经编译了共享对象haversine.so,我们可以使用ctypes在Python中加载它,我们需要提供文件路径来实现:

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

现在,haversine_lib.haversine 函数的作用与 Python 函数基本相同,只是我们可能需要进行一些手动类型转换以确保正确解释输入和输出。

numpy 实际上提供了一些不错的工具来实现这一点,这里我将使用的是 numpy.ctypeslib。我们将构建一个指针类型,使我们能够像使用指针一样传递 numpy.ndarrays 到这些 ctypes 加载的函数中。以下是代码:

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] 

请注意,我们告诉 haversine_lib.haversine 函数代理根据我们想要的类型解释其参数。

现在,为了从 Python 中进行测试,我们只需要创建一个大小变量和一个数组(就像在 C 代码中一样)来存储结果数据,然后调用它即可:

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

haversine.py__main__块中,将所有内容都放在一起,整个文件现在看起来像这样:

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

要运行它,分别运行并计时Python和ctypes版本,然后打印一些结果,我们只需要执行以下命令:

python haversine.py

显示以下内容:

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]

预期的结果是,numpy版本略微更快(对于长度为1百万的向量,需要0.11秒),但是我们简单粗暴的ctypes版本也表现不俗:在同样的数据上表现出色,只需要0.148秒。

现在,让我们将这与Python中朴素的for循环解决方案进行比较:

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

当我将这个放到与其他代码放在同一个Python文件中,并对同样的百万元素数据进行计时,我总是看到我的机器上大约2.65秒的时间。

因此,通过快速切换到ctypes,我们将速度提高了约18倍。对于许多可以受益于访问裸、连续数据的计算,您通常会看到比这更高的收益。

仅仅为了非常明确,我并不完全认可这比仅仅使用numpy更好。这正是numpy所致力于解决的问题,因此在以下两种情况下自己开发ctypes代码既不太有效(a)在您的应用程序中合并numpy数据类型有意义;(b)存在一种简单的方法将您的代码映射到numpy等效形式中。

但对于那些希望以C语言编写某些内容但又需要在Python中调用它的场合(或在嵌入式系统中无法安装numpy的情况下),掌握如何做这件事仍然非常有帮助。


这太棒了! - NFern

15

如果允许使用scikit-learn,我会尝试以下内容:

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))

3
请注意参数的顺序应为纬度,经度,与许多GIS库不同。 - MCMZL

9
一个简单的扩展@derricw的向量化解决方案,你可以使用numba来提高性能,几乎不需要改变你的代码,性能可以提高大约2倍。对于纯数值计算,这可能应该用于基准测试/测试,以便与可能更有效的解决方案进行比较。
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))

基准测试与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

完整的基准测试代码:
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

3
向量化函数指定“所有参数必须具有相等的长度”。根据此文,通过扩展“更大”数据集的边界,可以有效地找到所有i,j元素对的距离。请注意保留HTML标记。
from random import uniform
import numpy as np

def new_haversine_np(lon1, lat1, lon2, lat2):
    """
    Calculate the great circle distance between two points
    on the earth (specified in decimal degrees)

    """
    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])

    dlon = lon2 - lon1[:,None]

    dlat = lat2 - lat1[:,None]

    a = np.sin(dlat/2.0)**2 + np.cos(lat1[:,None]) * np.cos(lat2) * np.sin(dlon/2.0)**2

    c = 2 * np.arcsin(np.sqrt(a))
    km = 6367 * c
    return km

lon1 = [uniform(-180,180) for n in range(6)]
lat1 = [uniform(-90, 90) for n in range(6)]
lon2 = [uniform(-180,180) for n in range(4)]
lat2 = [uniform(-90, 90) for n in range(4)]

new = new_haversine_np(lon1, lat1, lon2, lat2)

for i in range(6):
    for j in range(4):
        print(i,j,round(new[i,j],2))

2

Scikit-learn库还有另一个用于计算haversine距离的函数,称为haversine_distances函数,可以用于查找两个坐标之间的距离,如下面的示例所示:

from sklearn.metrics.pairwise import haversine_distances
import numpy as np

lat1, lon1 = [-34.83333, -58.5166646]
lat2, lon2 = [49.0083899664, 2.53844117956]
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])

result = haversine_distances([[lat1, lon1], [lat2, lon2]])[0][1]  # this formula returns a 2x2 array, this is the reason we used indexing.
print(result * 6373)  # multiply by Earth radius to get kilometers

0

这些答案中有一些是“四舍五入”地处理地球半径。如果你将它们与其他距离计算器(如geopy)进行比较,这些函数的结果会有所偏差。

如果你想要英里作为答案,可以将R=3959.87433替换为下面的转换常数。

如果你想要公里作为答案,请使用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))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接