高速ハーバシン近似(Python/Pandas)

Pandasデータフレームの各行は2点の緯度/経度座標を含みます。以下のPythonコードを使用して、何百万もの行についてこれら2点間の距離を計算するのは非常に長い時間がかかります。

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'])
19
コメントの中の提案がうわー! sqrt((lat2 - lat1)^^ 2 +(lon2 - lon1)^^ 2)しかできないのでしょうか。すべてのポイントはニューヨーク市の中にあります。
追加された 著者 Nyxynyx,
@ballsdotballsユークリッド距離が私のために働くようですね! sqrt((lat2 - lat1)^^ 2 +(lon2 - lon1)^^ 2)の結果をマイルに変換する方法
追加された 著者 Nyxynyx,
近似よりも良い方法は、関数をプロファイルして時間がかかる理由を正確に把握した後、ctypes/Cython/numbaを使用して関数をそのままオーバーヘッドなしで実行されるC関数に変換することです。データの各パンダの Series 列の基礎となるデータのnumpy配列 values を使用するように呼び出し規約を変更する必要があるかもしれません。また、 numpy.ctypeslib <�チェックアウトすることもできます。 numpy配列からctypes互換の配列への変換を簡単にするため。それはたくさんのように思えますが、実際にはPythonでCの関数にアクセスするためのとても簡単な方法です。
追加された 著者 ely,
DataFrameのようなリレーショナル構造に格納するのではなく、データからk-dツリーを構築することも検討できます。それからそれは与えられたポイントの隣人を得るのは安いでしょう、そしておそらくあなたは要求に応じて距離を計算することしかできませんでした。アプリケーションは常にすべてのペアを必要としますか?さらに別の選択肢は、点をクラスタ化し、各クラスタの重心/平均をプロキシとして使用することであり得る。その場合、任意の2点間の距離は、クラスタ中心間の距離だけで近似されます。このような空想がブルートフォースよりも本当に優れているかどうかは推測的です。
追加された 著者 ely,
クラスタリングの場合は、同じクラスタ内のすべてのペアについて(各クラスタごとに)実際の距離を計算し、それらの点の有効距離がゼロにならないようにすることもできます。これは単純な並列化にも適しています。各プロセスにデータのコピー(またはコピーをロードさせる)を、それが担当するインデックスのリストと共に渡します。次に、そのプロセスは他のすべてのインデックスに対するそれらのインデックスのペアワイズ距離を計算し、どこかにそれらを書き込みます。わずか数百万行しかないので、これは控えめなハードウェアでは妥当なはずです。
追加された 著者 ely,
ええ、ユークリッド近似は十分に小さい距離ではうまくいきます。そのために apply を実行する必要さえないはずです。データフレームの列を直接使用するだけです。
追加された 著者 reptilicus,
大多数の候補者について計算をすることを回避することは可能であり得る。出発地から50マイルの最小および最大経度と緯度を計算します。次に、それらの最小値と最大値を使用して、候補者の大部分を排除します。
追加された 著者 Steven Rumbalski,
pypiには haversine モジュールがあります。 Cで実装
追加された 著者 Steven Rumbalski,
あなたのlat1、long1、lat1、long1はすべて同じ行で計算されていますか?
追加された 著者 dustin,
@Nyxynyxあなたがあなたの質問で提供した機能は大円距離を与えます。あなたのコメントの計算は、ユークリッド距離を与えます。地球の半径がとても大きいので、あなたは絶対的にeuclideanバージョンと短い距離で近似することができる。
追加された 著者 derricw,

7 答え

これは、同じ関数のベクトル化された派手なバージョンです。

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

入力はすべて値の配列であり、瞬時に数百万ポイントを実行できるはずです。必要条件は入力がndarraysであるがあなたのパンダテーブルの列がうまくいくということです。

たとえば、ランダムに生成された値では、

>>> 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は、データの配列全体を操作する機能を提供します。これにより、ループを回避し、パフォーマンスを大幅に向上させることができます。

これはベクトル化の例です。

44
追加された
大きさの異なる配列でも実際に動作することを知っている
追加された 著者 LetsPlayYahtzee,
どうもありがとうございました。ちょっとした提案:入力フォーマットを明確にするために、ランダムな値の代わりに実際の座標を使った実際の使用例を追加します。
追加された 著者 Dennis Golomazov,
これは、一対の引数が Series でもう一方がタプルの場合にも機能します。 haversine_np(pd.Series([ - 74.00594、-122.41942])、pd.Series([40.71278 (37.77493])、-87.65005、41.85003)は、(New York、San Francisco)とシカゴの間の距離を計算します。
追加された 著者 Dennis Golomazov,
もう一つのマイナーな提案:あなたは、関数の引数の順番をlat、lonに交換したいかもしれません。多くの情報源では緯度が最初になります。 ja.wikipedia.org/wiki/Horizo​​ntal_position_representation にあります。
追加された 著者 Dennis Golomazov,
その配列プログラミングという用語について知っておくのは良いことですが、それはMATLABにはありませんでした。
追加された 著者 Divakar,
@DennisGolomazovはGISのライブラリやソフトウェアを通常通り、緯度、経度で注文します。 Latitudeは、最初は主にGoogleマップの選択のようです。私は個人的にそれが緯度、経度と変わっていると思います。
追加された 著者 Leandro Lima,

純粋に例示的な例のために、@ ballsdotballsからの答えとして numpy バージョンを取り、また ctypes を介して呼び出されるコンパニオンC実装を作りました。 numpy はそのように高度に最適化されたツールなので、私のCコードがそれほど効率的になる可能性はほとんどありませんが、多少近いはずです。ここでの大きな利点は、C型を使った例を実行することによって、あまりにも多くのオーバーヘッドをかけずに、自分自身の個人用C関数をPythonに接続する方法を理解するのに役立つことです。これは、PythonではなくCのソースで小さな部分を書くことによって、小さな部分の大きな計算を最適化したいときに特に便利です。ほとんどの場合、単に numpy を使用するだけで問題は解決しますが、 numpy をすべて必要としていない場合やカップリングを追加したくない場合は、一部のコードで numpy データ型を使用する必要がある場合は、組み込みの ctypes ライブラリにドロップダウンして自分で行う方法を知っておくと非常に便利です。

まず、 haversine.c というCソースファイルを作成しましょう。

#include 
#include 
#include 

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 を使用し、期待されるデータが含まれるように渡された入力の1つを変更することによって 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の間でランダムに選択された緯度と経度(度)を作成することにしましたが、この説明にはそれほど問題ではありません。

次に行う必要があるのは、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 をコンパイルしたので、これをPythonでロードするために ctypes を使用できます。そのためには、ファイルへのパスを指定する必要があります。

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

これで、 haversine_lib.haversine はPython関数とほとんど同じように機能します。ただし、入力と出力が正しく解釈されるようにするには、手動のタイプマーシャリングを行う必要がある場合があります。

numpy actually provides some nice tools for this and the one I'll use here is numpy.ctypeslib. We're going to build a pointer type that will allow us to pass around numpy.ndarrays to these ctypes-loaded functions as through they were pointers. Here's the 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] 

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
 [  231.53675618  3042.84723579   169.51575588 ...,  1359.26453029
  2686.87709456  3728.54493339]

予想どおり、 numpy バージョンはやや高速です(長さ100万のベクトルの場合0.11秒)が、私たちのすばやく汚い ctypes バージョンはそれほど難しくありません。同じデータ。

これを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 コードを自作することは numpy アプリケーションのデータ型と(b)コードを 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))
7
追加された

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))
7
追加された

これらの答えのいくつかは地球の半径を「丸める」ことです。これらを他の距離計算機( 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
追加された

これらの答えのいくつかは地球の半径を「丸める」ことです。これらを他の距離計算機( 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
追加された

これらの答えのいくつかは地球の半径を「丸める」ことです。これらを他の距離計算機( 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
追加された