あなたが何をしているのか見てみましょう。
すべてのポイントを という名前のリストに読み込みますgeo_points
。
では、リストがソートされているかどうか教えていただけますか? それがソートされた場合、私たちは間違いなくそれを知りたいからです。並べ替えは貴重な情報であり、特に 500 万件ものものを扱っている場合はそうです。
すべてをループしますgeo_points
。あなたによると、それは500万です。
外側のループ内で、500 万個すべてを再度geo_points
ループします。
2 つのループ アイテム間の距離をマイル単位で計算します。
距離がしきい値よりも小さい場合は、最初のポイントでその情報を記録し、内側のループを停止します。
内側のループが停止したら、外側のループ項目に関する情報を CSV ファイルに書き込みます。
いくつかのことに注意してください。まず、外側のループで 500 万回ループしています。そして、内側のループで 500 万回ループしています。
これが O(n²) の意味です。
次に誰かが「これは O(log n) ですが、それ以外は O(n log n) です」と話しているのを見かけたら、この経験を思い出してください。この場合、n が 5,000,000 である n² アルゴリズムを実行していることになります。クソ、ダニット?
とにかく、あなたにはいくつかの問題があります。
問題 1: 最終的には、すべてのポイントをそれ自体と比較することになります。これは、距離がゼロであるべきです。つまり、それらはすべて、距離のしきい値内にあるとマークされます。プログラムが終了すると、すべてのセルが True とマークされます。
問題 2: ポイント #1 をポイント #12345 と比較し、それらが互いにしきい値距離内にある場合、ポイント #1 に関する情報を記録していることになります。しかし、他の点について同じ情報を記録することはありません。ポイント #12345 (geo_point2) が反射的にポイント #1 のしきい値内にあることはわかっていますが、それを書き留めていません。つまり、500 万回以上の比較をスキップするチャンスを逃しています。
問題 3: ポイント #1 とポイント #2 を比較し、それらがしきい値距離内にない場合、ポイント #2 とポイント #1 を比較するとどうなりますか? 内部ループは毎回リストの先頭から開始していますが、リストの先頭とリストの末尾を既に比較していることはわかっています。i in range(0, 5million)
外側のループを goにし、内側のループを go にするだけで、問題のスペースを半分に減らすことができますj in range(i+1, 5million)
。
答えは?
平面上の緯度と経度を考慮してください。5 マイル以内にポイントがあるかどうかを知りたいとします。ポイント 1 を中心とする 10 マイルの正方形について考えてみましょう。これは、(X1, Y1) を中心とする正方形で、左上隅が (X1 - 5 マイル、Y1 + 5 マイル) で、右下隅が (X1 + 5 マイル、Y1 - 5 マイル) です。ポイントがその正方形内にある場合、ポイント #1 から 5 マイル以内にない可能性があります。しかし、その広場の外にある場合は、5 マイル以上離れていることは間違いありません。
@SeverinPappadeaux が指摘しているように、地球のような回転楕円体での距離は、平面での距離とまったく同じではありません。しかし、だから何?違いを考慮して正方形を少し大きく設定し、先に進みます!
ソート済みリスト
これが、ソートが重要な理由です。すべてのポイントが X、次に Y (または Y、次に X など) でソートされていて、それを知っていれば、本当にスピードアップできます。X (または Y) 座標が大きくなりすぎたときにスキャンを停止するだけで、500 万ポイントを通過する必要がないからです。
それはどのように機能しますか?前と同じ方法ですが、内側のループには次のようなチェックがあります。
five_miles = ... # Whatever math, plus an error allowance!
list_len = len(geo_points) # Don't call this 5 million times
for i, pi in enumerate(geo_points):
if pi.close_to_another_point:
continue # Remember if close to an earlier point
pi0max = pi[0] + five_miles
pi1min = pi[1] - five_miles
pi1max = pi[1] + five_miles
for j in range(i+1, list_len):
pj = geo_points[j]
# Assumes geo_points is sorted on [0] then [1]
if pj[0] > pi0max:
# Can't possibly be close enough, nor any later points
break
if pj[1] < pi1min or pj[1] > pi1max:
# Can't be close enough, but a later point might be
continue
# Now do "real" comparison using accurate functions.
if ...:
pi.close_to_another_point = True
pj.close_to_another_point = True
break
私はそこで何をしているのですか?まず、いくつかの数値をローカル変数に入れています。次に、値と外側の点への参照をenumerate
与えるために使用しています。(あなたが呼んだもの)。次に、このポイントが別のポイントに近いことをすでに知っているかどうかをすばやく確認しています。i
geo_point
そうでない場合は、スキャンする必要があります。したがって、リスト内の「後の」ポイントのみをスキャンしています。これは、外側のループが初期のポイントをスキャンすることを知っているためです。ポイントをそれ自体と比較したくないことは間違いありません。いくつかの一時変数を使用して、外側のループを含む計算の結果をキャッシュしています。内側のループ内で、一時オブジェクトに対していくつかのばかげた比較を行います。2 つの点が互いに近いかどうかはわかりませんが、確実に近くないかどうかを確認してスキップすることはできます。
最後に、単純なチェックに合格した場合は、先に進んで高価なチェックを行います。実際にチェックに合格した場合は、後で 2 番目のポイントをスキップできるように、必ず両方のポイントで結果を記録してください。
ソートされていないリスト
しかし、リストがソートされていない場合はどうなるでしょうか?
@RootTwo は、kD ツリーを示します (ここで、D は「次元」を表し、この場合の k は「2」です)。二分探索木について既に知っている場合、アイデアは非常に単純です。次元を循環し、ツリーの偶数レベルで X を比較し、奇数レベルで Y を比較します (またはその逆)。アイデアは次のようになります。
def insert_node(node, treenode, depth=0):
dimension = depth % 2 # even/odd -> lat/long
dn = node.coord[dimension]
dt = treenode.coord[dimension]
if dn < dt:
# go left
if treenode.left is None:
treenode.left = node
else:
insert_node(node, treenode.left, depth+1)
else:
# go right
if treenode.right is None:
treenode.right = node
else:
insert_node(node, treenode.right, depth+1)
これは何をしますか?これにより、O(log n)時間でポイントを挿入できる検索可能なツリーが得られます。これは、リスト全体で O(n log n) を意味し、n の 2 乗よりも優れています。(500 万の対数底 2 は基本的に 23 です。したがって、n log n は 500 万 x 23 であり、500 万 x 500 万です!)
また、ターゲットを絞った検索を実行できることも意味します。ツリーは順序付けられているため、「近い」ポイントを探すのはかなり簡単です (@RootTwo からのウィキペディア リンクでアルゴリズムが提供されます)。
アドバイス
私のアドバイスは、必要に応じてリストをソートするコードを書くことです。書きやすく、手で確認しやすく、一度だけ作成する必要がある別のパスです。
リストを並べ替えたら、上で示した方法を試してください。それはあなたがしていたことに近く、理解しやすく、コーディングしやすいはずです。