Pandasデータフレームの各行には、2ポイントの緯度/経度座標が含まれます。以下の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'])
同じ関数のベクトル化された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
入力はすべて値の配列であり、数百万のポイントを即座に実行できる必要があります。要件は、入力が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'])
Pythonでは、データの配列をループするのが非常に遅くなります。 Numpyは、データ配列全体を操作する機能を提供します。これにより、ループを回避し、パフォーマンスを大幅に向上させることができます。
これは vectorization の例です。
単に説明のために、@ ballsdotballsからの回答でnumpy
バージョンを取得し、ctypes
経由で呼び出されるコンパニオンC実装も作成しました。 numpy
は非常に最適化されたツールであるため、Cコードが同じくらい効率的である可能性はほとんどありませんが、多少近いはずです。ここでの大きな利点は、C型を使用して例を実行することで、独自のC関数をPythonにオーバーヘッドをかけずに接続する方法を確認するのに役立ちます。これは特に素晴らしいです。 PythonでなくCソースで小さな部分を書くことで、大きな計算の小さな部分を最適化するだけの場合。ほとんどの場合、numpy
を使用するだけで問題は解決しますが、そうしない場合は'numpy
のすべては本当に必要ではなく、いくつかのコード全体でnumpy
データ型の使用を要求するカップリングを追加したくないので、ビルド済みにドロップダウンする方法を知ることは非常に便利です-in 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
を使用して、参照によってデータ引数を明示的に渡し、渡された入力の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
をコンパイルしたので、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
です。pointer typeを作成して、numpy.ndarrays
をこれらのctypes
- loaded関数に渡します。ポインター。コードは次のとおりです。
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
関数プロキシに、必要な型に従って引数を解釈するように指示していることに注意してください。
さて、それをテストするためにfrom 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__
の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
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)アプリケーションにctypes
データ型を組み込むことが理にかなっている場合は、独自のnumpy
コードを自作する(b)コードをnumpy
に等価にマップする簡単な方法がありますが、あまり効率的ではありません。
ただし、Cで何かを書いてPythonで呼び出す場合、またはnumpy
への依存が実用的でない状況(組み込みシステムでnumpy
はインストールできません。たとえば)。
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))
@ 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
ベクトル化された関数は、「すべての引数は同じ長さでなければならない」ことを指定します。 this に従って「より大きい」データセットの境界を拡張することにより、要素のすべてのi、jペアの距離を効率的に見つけることができます。
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))
これらの答えのいくつかは、地球の半径を「丸め」ます。これらを他の距離計算機(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))