【问题标题】:Is there a faster way than for loops and if statements to find nearest point to another point in python?有没有比 for 循环和 if 语句更快的方法来查找到 python 中另一个点的最近点?
【发布时间】:2020-08-24 08:59:50
【问题描述】:

是否有更快的方法(在 Python 中,使用 CPU)来执行与以下函数相同的操作?我使用了For 循环和if 语句,想知道是否有更快的方法?目前每 100 个邮政编码需要大约 1 分钟来运行此功能,而我大约有 70,000 个要通过。

使用的 2 个数据框是:

postcode_df 包含 71,092 行和列:

  • 邮政编码例如“BL4 7PD”
  • 纬度,例如53.577653
  • 经度,例如-2.434136

例如

postcode_df = pd.DataFrame({"Postcode":["SK12 2LH", "SK7 6LQ"],
                                    "Latitude":[53.362549, 53.373812],
                                    "Longitude":[-2.061329, -2.120956]})

air 包含 421 行和列:

  • TubeRef 例如“ABC01”
  • 纬度,例如53.55108
  • 经度,例如-2.396236

例如

air = pd.DataFrame({"TubeRef":["Stkprt35", "Stkprt07", "Stkprt33"],
                                    "Latitude":[53.365085, 53.379502, 53.407510],
                                    "Longitude":[-2.0763, -2.120777, -2.145632]})

该函数循环遍历 postcode_df 中的每个邮政编码,并为每个邮政编码循环遍历每个 TubeRef 并计算(使用geopy)它们之间的距离,并保存与邮政编码距离最短的 TubeRef。

输出 df,postcode_nearest_tube_refs,包含每个邮政编码最近的管并包含列:

  • 邮政编码例如“BL4 7PD”
  • 最近的空气管,例如"ABC01
  • 到空气管 KM 的距离,例如1.035848
# define function to get nearest air quality monitoring tube per postcode
def get_nearest_tubes(constituency_list):
    
    postcodes = []
    nearest_tubes = []
    distances_to_tubes = []
    
    for postcode in postcode_df["Postcode"]:
            closest_tube = ""
            shortest_dist = 500

            postcode_lat = postcode_df.loc[postcode_df["Postcode"]==postcode, "Latitude"]
            postcode_long = postcode_df.loc[postcode_df["Postcode"]==postcode, "Longitude"]
            postcode_coord = (float(postcode_lat), float(postcode_long))


            for tuberef in air["TubeRef"]:
                tube_lat = air.loc[air["TubeRef"]==tuberef, "Latitude"]
                tube_long = air.loc[air["TubeRef"]==tuberef, "Longitude"]
                tube_coord = (float(tube_lat), float(tube_long))

                # calculate distance between postcode and tube
                dist_to_tube = geopy.distance.distance(postcode_coord, tube_coord).km
                if dist_to_tube < shortest_dist:
                    shortest_dist = dist_to_tube
                    closest_tube = str(tuberef)

            # save postcode's tuberef with shortest distance
            postcodes.append(str(postcode))
            nearest_tubes.append(str(closest_tube))
            distances_to_tubes.append(shortest_dist)
            
    # create dataframe of the postcodes, nearest tuberefs and distance
    postcode_nearest_tube_refs = pd.DataFrame({"Postcode":postcodes, 
                                          "Nearest Air Tube":nearest_tubes, 
                                          "Distance to Air Tube KM": distances_to_tubes})

    return postcode_nearest_tube_refs

我正在使用的库是:

import numpy as np
import pandas as pd
# !pip install geopy
import geopy.distance

【问题讨论】:

  • 使用示例输入和预期输出更新了您的帖子
  • geopandas 包提供空间索引。 Lesson 3 of the AutoGIS 2019 course 涵盖地理编码和 r-trees 的使用。
  • 不要计算全距离矩阵,使用 BallTree 算法。 scikit-learn.org/stable/modules/generated/… 它支持半正弦距离,并且比全距离矩阵要好得多。我的猜测是这需要几秒钟/几分钟。如果您需要一个完整的工作示例,请告诉我。请提供一些对熊猫友好的数据行
  • @user3184950 (stackoverflow.com/users/3184950/user3184950) 谢谢。我已经用 Pandas 代码更新了这个问题,用于创建带有一些示例行的输入数据框。这能满足你的需要吗?很高兴看到一个完整的工作示例。
  • 是的,这有帮助。我发布了它,结果不到 10 秒。

标签: python performance geopy


【解决方案1】:

这里是一个工作示例,需要几秒钟 (

导入库

import pandas as pd
import numpy as np
from sklearn.neighbors import BallTree
import uuid

我生成了一些随机数据,这也需要一秒钟,但至少我们有一些实际的数量。

np_rand_post = 5 * np.random.random((72000,2))
np_rand_post = np_rand_post + np.array((53.577653, -2.434136))

并使用 UUID 伪造邮政编码

postcode_df = pd.DataFrame( np_rand_post , columns=['lat', 'long'])
postcode_df['postcode'] = [uuid.uuid4().hex[:6] for _ in range(72000)]
postcode_df.head()

我们对空气也是如此

np_rand = 5 * np.random.random((500,2))
np_rand = np_rand + np.array((53.55108, -2.396236))

再次使用 uuid 来伪造 ref

tube_df = pd.DataFrame( np_rand , columns=['lat', 'long'])
tube_df['ref'] = [uuid.uuid4().hex[:5] for _ in range(500)]
tube_df.head()

将 gps 值提取为 numpy

postcode_gps = postcode_df[["lat", "long"]].values
air_gps = tube_df[["lat", "long"]].values

创建一个球树

postal_radians =  np.radians(postcode_gps)
air_radians = np.radians(air_gps)

tree = BallTree(air_radians, leaf_size=15, metric='haversine')

查询最接近的优先

distance, index = tree.query(postal_radians, k=1)

注意距离不是KM,需要先换算。

earth_radius = 6371000
distance_in_meters = distance * earth_radius
distance_in_meters

例如使用tube_df.ref[ index[:,0] ]获取参考

【讨论】:

  • 谢谢。如果可行,我会尝试并标记为答案。
  • 这非常有效,几乎是瞬间的,它是如此之快!非常感谢!
  • 在这种情况下机器速度相当快!很高兴它为标记工作!
【解决方案2】:

您可以使用 numpy 计算集合 A 中的任何点到集合 B 中的任何点的距离矩阵,然后只取集合 A 中与最小距离相对应的点。

import numpy as np
import pandas as pd

dfA = pd.DataFrame({'lat':np.random.uniform(0, 30, 3), 'lon':np.random.uniform(0, 30, 3), 'id':[1,2,3]})
dfB = pd.DataFrame({'lat':np.random.uniform(0, 30, 3), 'lon':np.random.uniform(0, 30, 3), 'id':['a', 'b', 'c']})
lat1 = dfA.lat.values.reshape(-1, 1)
lat2 = dfB.lat.values.reshape(1, -1)
lon1 = dfA.lon.values.reshape(-1, 1)
lon2 = dfB.lon.values.reshape(1, -1)
dists = np.sqrt((lat1 - lat2)**2 + (lon1-lon2)**2)
for id1, id2 in zip (dfB.id, dfA.id.iloc[np.argmin(dists, axis=1)]):
    print(f'the closest point in dfA to {id1} is {id2}')

【讨论】:

  • 这仅在 dfA 和 dfB 长度相同时才有效吗?我收到错误“IndexError:位置索引器超出范围”尝试使用包含比 dfB 更多行的 dfA 进行此操作
  • 它不依赖于A和B的长度,你可以尝试我的解决方案用不同长度的数据框。
猜你喜欢
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-01
  • 2012-02-12
  • 1970-01-01
  • 2012-12-02
  • 2021-04-13
相关资源
最近更新 更多