快速的Haversine逼近(Python / Pandas)

时间:2015-04-09 18:00:08

标签: python numpy pandas gis haversine

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

6 个答案:

答案 0 :(得分:57)

这是同一功能的矢量化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的一个例子。

答案 1 :(得分:12)

纯粹为了一个说明性的例子,我从@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

将所有内容放在__main__的{​​{1}}块中,整个文件现在看起来像这样:

haversine.py

要运行它,它将分别运行和计算Python和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 版本并打印一些结果,我们可以这样做

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] 版本稍微快一些(长度为1百万的向量为0.11秒),但我们快速而肮脏的numpy版本并不懈怠:相同数据上的0.148秒可观

让我们将其与Python中的一个天真的for循环解决方案进行比较:

ctypes

当我将其放入与其他文件相同的Python文件中并将其计入相同的百万元素数据时,我一直在机器上看到大约2.65秒的时间。

因此,通过快速切换到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 ,我们将速度提高了大约18倍。对于许多可以从访问裸,连续数据中获益的计算,您经常会看到比这更高的收益。

只是要非常明确,我并不认为这是一个比使用ctypes更好的选择。这正是构建numpy以解决问题的问题,因此,只要它们(a)将numpy数据类型合并到您的应用程序中,就可以自行创建自己的ctypes代码(b) )有一种简单的方法可以将代码映射到numpy等价物,效率不高。

但是,如果你喜欢用C语言写一些东西但是用Python调用它,或者对numpy的依赖不实用的情况,那么知道如何做到这一点仍然非常有帮助(例如,在无法安装numpy的嵌入式系统中。

答案 2 :(得分:8)

如果允许使用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 :(得分:1)

@derricw's vectorised solution的简单扩展,您可以使用numba将性能提高约2倍,而无需更改代码。对于纯数值计算,应该将其用于基准测试/测试,而不是更有效的解决方案。

Field

基准化与熊猫功能:

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

答案 4 :(得分:1)

向量化函数指定“所有args的长度必须相等”。根据{{​​3}},通过扩展“较大”数据集的边界,可以有效地找到所有i,j个元素对的距离。

from django.conf import settings

from mysite.settings import settings as default
# use this could set default settings to active setting
settings.configure(default, DEBUG=True)

more dev settings.....

答案 5 :(得分:0)

其中一些答案是&#34;四舍五入&#34;地球的半径。如果您针对其他距离计算器(例如 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))