You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python中基于Shapely的海量点路匹配向量化高效实现咨询

需求说明

我在Python中有两个DataFrame:一个约15万条呼叫数据,每条都对应一个地理位置,另一个约5万条街道数据,每条都对应一条地理路径。目标是根据每个呼叫的位置,将最近街道的头节点、尾节点ID追加到呼叫DataFrame中。

现有实现

我已完成基础数据转换,对应以下两个算法,同时自行实现了最近街道匹配逻辑,但性能达不到要求:

算法一:呼叫点坐标转Shapely Point

% Algorithm One: given two columns of latitude & longitude, create a new Point 
def call_iter():
    points = []
    for index, row in calls.iterrows():
        points.append(Point(row['Incident Latitude'], row['Incident Longitude']))
    
    return points % appended to the call dataframe

算法二:街道字符串路径转Shapely LineString

% Algorithm Two: given a string column containing coordinate data, construct a LineString 
def street_iter():     
    paths = []
    for geo in streets.geometry:
        l = []
        for t in geo.split():
            try:
                t = t.strip('(,)')
                l.append(float(t))
            except ValueError:
                pass
    
        p = []
        for i in range(0, len(l), 2):
            p.append(Point(l[i], l[i+1]))
    
        paths.append(LineString(p))
    return paths  % appended to the street dataframe

算法三:最近街道匹配

依托Shapely的line.distance(point)方法实现匹配,功能正常但单条呼叫处理耗时1-2秒,全量15万条数据处理耗时过高,无法满足批量处理需求:

% Algorithm Three: find the closest street (head 'u' and tail 'v' nodes) to each call
def build_matrix():
    heads = []
    tails = []

    for i_c, r_c in calls.iterrows():
        print(i)
        p = r_c[4]
        head_min = -1
        tail_min = -1
        dist_min = float('inf')
        min_group = []
    
        for i_s, r_s in streets.iterrows():
            l = r_s[5].distance(p)
            if dist_min > l:
                head_min = r_s['u'] % head node
                tail_min = r_s['v'] % tail node
                dist_min = l
                min_group = []
                min_group.append(r_s)
            if dist_min == l:
                min_group.append(r_s)
            
        if len(min_group) > 1:
            choice = secrets.choice(min_group) % randomly selects an arc
            head_min = choice['u']
            tail_min = choice['v']
        
        heads.append(head_min)
        tails.append(tail_min)

    return (heads, tails) % both appended to the calls dataframe
优化方案

核心优化思路是用向量化操作替换Python层级的循环,用空间索引将匹配复杂度从O(MN)降到O(MlogN),全流程如下:

1. 前置准备

提前安装geopandas、rtree两个库,用于空间操作和空间索引。

2. 基础数据转换优化(替换算法一、算法二)

import geopandas as gpd
import re
from shapely.geometry import LineString
import numpy as np

# 算法一优化:批量生成呼叫点,无需逐行迭代
calls['geometry'] = gpd.points_from_xy(calls['Incident Longitude'], calls['Incident Latitude'])
# 转为GeoDataFrame,注意crs参数替换为你的实际坐标参考系,WGS84经纬度填EPSG:4326
calls_gdf = gpd.GeoDataFrame(calls, crs="EPSG:4326")

# 算法二优化:批量解析街道路径,替换多层循环
def parse_linestring(geo_str):
    # 正则批量提取所有数值,比逐字符处理效率高
    coords = list(map(float, re.findall(r'-?\d+\.?\d*', geo_str)))
    # 每两个数值组成一个坐标对
    point_pairs = list(zip(coords[::2], coords[1::2]))
    return LineString(point_pairs)

streets['geometry'] = streets['geometry'].apply(parse_linestring)
streets_gdf = gpd.GeoDataFrame(streets, crs=calls_gdf.crs)

3. 最近街道匹配优化(替换算法三)

用geopandas内置的最近邻空间连接方法,底层自动使用空间索引筛选候选,无需手动嵌套循环:

# 给街道加随机排序字段,处理多条街道距离完全相同时随机选择的需求
streets_gdf['random_sort'] = np.random.rand(len(streets_gdf))

# 最近邻空间连接,每个呼叫点匹配最近的街道
matched = gpd.sjoin_nearest(
    calls_gdf, 
    streets_gdf[['u', 'v', 'geometry', 'random_sort']],
    how='left',
    distance_col='distance_to_street' # 可保留匹配距离用于后续校验
)

# 处理同距离的情况:按距离、随机序排序后取第一个
matched = matched.sort_values(['distance_to_street', 'random_sort']).groupby(level=0).first()

# 把匹配到的头、尾节点追加到原呼叫DataFrame
calls[['street_head_u', 'street_tail_v']] = matched[['u', 'v']]

性能说明

原嵌套循环处理15万条呼叫需要几十甚至上百小时,优化后全流程仅需几秒到几十秒,可满足多批量数据处理需求。
如果需要计算米级的物理距离,可将两个GeoDataFrame先投影到对应地区的平面坐标系后再执行匹配。


内容的提问来源于stack exchange,提问作者Jordan MacLachlan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 02:45:07