首页 文章

快速的Haversine逼近(Python / Pandas)

提问于
浏览
23

Pandas数据帧中的每一行包含2个点的lat / lng坐标 . 使用下面的Python代码,计算许多(数百万)行的这两个点之间的距离需要很长时间!

考虑到2点相距不到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'])

5 回答

  • 52

    这是相同功能的矢量化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 = 6367 * c
        return km
    

    输入都是值的数组,它应该能够立即完成数百万个点 . 要求是输入是ndarray但是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'])
    

    在python中循环遍历数据数组非常慢 . Numpy提供了对整个数据数组进行操作的函数,可以避免循环并大幅提高性能 .

    这是vectorization的一个例子 .

  • 8

    纯粹为了一个说明性的例子,我从@ballsdotballs的答案中获取了 numpy 版本,并且还通过 ctypes 调用了一个伴随的C实现 . 由于 numpy 是一个高度优化的工具,因此我的C代码几乎不可能有效,但它应该有点接近 . 这里的一大优势是通过运行C类型的示例,它可以帮助您了解如何将自己的个人C函数连接到Python而不会产生太多开销 . 当你只想通过在一些C源而不是Python中编写那个小块来优化一小块更大的计算时,这是特别好的 . 简单地使用 numpy 将在大多数情况下解决问题,但是对于那些你真的不需要全部 numpy 并且你不想在一些代码中添加耦合以要求使用 numpy 数据类型的情况,它非常方便知道如何下载到内置的 ctypes 库并自己动手 .

    首先让我们创建一个名为 haversine.c 的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之间随机选择的lats和lons(以度为单位),但这对于这个解释并不重要 .

    接下来我们需要做的是以可以由Python动态加载的方式编译我们的C模块 . 我正在使用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 版本是稍微快一点(对于长度为100万的向量,为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 为解决而构建的问题,因此,只要它们(a)在您的应用程序中合并 numpy 数据类型是有意义的,并且(b)存在一种将代码映射到一个简单方法的简单方法,那么自己编写自己的 ctypes 代码 . 相当于 numpy ,效率不高 .

    但是,如果你喜欢用C编写一些东西但是用Python调用它,或者依赖于 numpy 不实用的情况(在无法安装 numpy 的嵌入式系统中,对于那些情况)知道如何执行此操作仍然非常有用 . 例) .

  • 11

    如果允许使用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))
    
  • 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))
    
  • 0

    @derricw's vectorised solution的简单扩展,您可以使用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))
    

    基准测试与熊猫功能:

    %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
    

相关问题