簡體   English   中英

從一個熊貓數據框中為第二個數據框中的每個觀測值找到最接近的(緯度/經度)觀測值

[英]Find closest (lat/lon) observation from one pandas dataframe for each observation in a second dataframe

問題總結:

我有兩個數據框。 第一個數據框 (df1) 相對較小(幾乎總是少於 100 個觀測值,通常少於 50 個),具有一組點標識符及其緯度/經度坐標。 第二個數據框 (df2) 非常大(數十萬個觀測值),它也具有緯度/經度坐標。 我希望在 df2 中創建兩個新列:第一個具有距 df1 最近點的標識符,第二個具有到該點的距離。 我目前的方法很笨拙,我認為可以顯着優化。 對於其他上下文,有一個 df1(小數據幀),但我將對多個 df2(大數據幀)重復此過程。

設置/樣本數據:

# imports:
import pandas as pd
import geopy.distance
from faker import Faker

# creating sample data:
Faker.seed(0)
fake=Faker()

id1=[]
lat1=[]
lon1=[]
id2=[]
lat2=[]
lon2=[]
length1=10 # length of df1
length2=100 # length of df2

for x in range(length1):
    a=fake.local_latlng()
    id1.append(x)
    lat1.append(float(a[0]))
    lon1.append(float(a[1]))
for x in range(length2):
    a=fake.local_latlng()
    id2.append(x)
    lat2.append(float(a[0]))
    lon2.append(float(a[1]))

dict1={
    'loc_id' : id1,
    'lat' : lat1,
    'lon' : lon1,
    }

dict2={
    'point_id' : id2,
    'lat' : lat2,
    'lon' : lon2,
    }

df1=pd.DataFrame(dict1)
df2=pd.DataFrame(dict2)

當前解決方案:

# calculating distances:
for x in range(len(df1)):
    loc_id=df1.iloc[x]['loc_id']
    pt1=(df1.iloc[x]['lat'],df1.iloc[x]['lon'])
    for y in range(len(df2)):
        pt2=(df2.iloc[y]['lat'],df2.iloc[y]['lon'])
        dist=geopy.distance.distance(pt1,pt2).miles
        df2.loc[y,x]=dist

# determining minimum distance and label:
temp_cols=list(range(len(df1)))
df2['min_dist']=df2[temp_cols].min(axis=1)
df2['min_loc']=df2[temp_cols].idxmin(axis=1)

# removing extra columns:
df2=df2.drop(temp_cols,axis=1)
print(df2.head())

可能的解決方案:

這段代碼顯然很慢,因為我計算了每對點的距離。 從概念上講,我認為這可以改進,但我在實施改進時遇到了麻煩。 一些想法:

  1. 矢量化操作。 這個接受的答案似乎表明對向量的操作更快,但我不知道如何在向量上實現 geopy.distance.distance() 函數(或者如果可能的話)。
  2. 通過比較哪些點被“支配”可以這么說來消除點。 這樣,例如,如果一個點在緯度/經度上都比另一個點大,那么當與我必須檢查的集合中兩個緯度/經度點中都較小的點進行比較時,我可能能夠消除它。 我想這會增加前端的工作/處理,但最終會通過減少我為每個點檢查的點數來獲得回報。 不過,弄清楚該算法對我來說並不明顯。
  3. 我也許能夠將點分入彼此附近的組中,從而獲得更小的候選集以進行相互比較。 也許有可能在計算距離之前找出最近的點。 危險在於 df1 中的某些點也可能非常接近。

附加細節:兩點具有相同距離的幾率很小,如果應該出現的話,我很高興隨機選擇任何與最接近的點。

基於使用Balltree 的k 最近鄰

方法

  1. 為 df1 的緯度/經度創建 k 最近鄰樹。 使用 BallTree,因為它允許自定義距離函數,例如 geopy.distance.distance
  2. 對於 df2 中的每個緯度/經度,從 1 中找到它在樹中最近的點
  3. 注意:如果我們使用內置的距離函數,例如Haversine,Balltree 會更快

代碼

import pandas as pd
import numpy as np
from faker import Faker
from sklearn.neighbors import BallTree
from geopy import distance

import functools
import time

# Timing Decorator
def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

def generate_data(length):
    '''
        Genearte data frame with random lat/lon data 
        Data with same length is always identical since we use same initial seed
    '''
    Faker.seed(0)
    fake = Faker()

    id = list(range(length))
    # First two fields of fake.local_latlng are lat & lon as string
    # Generate vector of fake.local_latlng then unpack out lat/lon array
    lat, lon = list(zip(*[fake.local_latlng() for _ in range(length)]))[:2]
    
    # Convert strings to float
    lat = [float(x) for x in lat]
    lon = [float(x) for x in lon]
    
    return pd.DataFrame({'point_id':id, 'lat':lat, 'lon':lon})

def generate_balltree(df):
    '''
        Generate Balltree using customize distance (i.e. Geodesic distance)
    '''
    return  BallTree(df[['lat', 'lon']].values, metric=lambda u, v: distance.distance(u, v).miles)

@timer
def find_matches(tree, df):
    '''
        Find closest matches in df to items in tree
    '''
    distances, indices = tree.query(df[['lat', 'lon']].values, k = 1)
    df['min_dist'] = distances
    df['min_loc'] = indices
    
    return df

@timer
def find_min_op(df1, df2):
    ' OP solution (to compare timing) '

    for x in range(len(df1)):
        #loc_id=df1.iloc[x]['loc_id'] # not used
        pt1=(df1.iloc[x]['lat'],df1.iloc[x]['lon'])
        for y in range(len(df2)):
            pt2=(df2.iloc[y]['lat'],df2.iloc[y]['lon'])
            dist=distance.distance(pt1,pt2).miles
            df2.loc[y,x]=dist

    # determining minimum distance and label:
    temp_cols=list(range(len(df1)))
    df2['min_dist']=df2[temp_cols].min(axis=1)
    df2['min_loc']=df2[temp_cols].idxmin(axis=1)

    # removing extra columns:
    df2 = df2.drop(columns = temp_cols)
    
    return df2

測試

df1 = 100 個元素,df2 = 1000 個元素

l1, l2 = 100, 1000
df1 = generate_data(l1)
df2 = generate_data(l2)
tree = generate_balltree(df1)
find_matches(tree, df2)

df2 = generate_data(l2)  # Regenerate df2 for next test since find_matches modified it
find_min_op(df1, df2)

輸出

Finished 'find_matches' in 32.1677 secs
Finished 'find_min_op' in 147.7042 secs

因此,這種方法在這個測試中快了 5 倍

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM