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

优化DataFrame坐标行距离匹配:替代双重循环的高效实现方案

高效优化方案

原代码的双重循环时间复杂度为O(n²),数据量稍大就会严重卡顿,以下是两种更高效的实现思路:

方案一:用scipy的KDTree实现球面距离查询

KDTree可快速完成最近邻搜索,结合haversine公式计算球面距离,适合大数据量场景。

步骤:

  • 先过滤经纬度缺失的行,或标记这些行的邻居为空
  • 将经纬度转换为弧度(KDTree的haversine参数要求输入弧度)
  • 构建KDTree,批量查询每个点在不同距离阈值内的邻居
  • 映射回原DataFrame的id,生成结果列

代码示例:

import pandas as pd
import numpy as np
from scipy.spatial import cKDTree

# 定义haversine距离计算(返回公里数)
def haversine(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)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
    c = 2 * np.arcsin(np.sqrt(a))
    r = 6371  # 地球半径(公里)
    return c * r

# 加载数据并处理缺失值
df = pd.read_csv("coordinates.csv", na_values=None)
valid_mask = df[['longitude', 'latitude']].notna().all(axis=1)
valid_coords = df.loc[valid_mask, ['latitude', 'longitude']].values
valid_coords_rad = np.radians(valid_coords)

# 构建KDTree
tree = cKDTree(valid_coords_rad, leafsize=10)

# 距离阈值转换为球面弧度差(KDTree查询用弧度)
thresholds = {
    '1km': 1 / 6371,
    '5km': 5 / 6371,
    '10km': 10 / 6371,
    '50km': 50 / 6371
}

# 初始化结果列
for col in thresholds.keys():
    df[col] = [[] for _ in range(len(df))]

# 批量查询有效坐标的邻居
valid_indices = df[valid_mask].index.tolist()
for idx in valid_indices:
    point_idx_in_valid = valid_indices.index(idx)
    point = valid_coords_rad[point_idx_in_valid]
    for dist_name, rad_dist in thresholds.items():
        neighbor_positions = tree.query_ball_point(point, rad_dist)
        # 排除自身
        neighbor_positions = [p for p in neighbor_positions if p != point_idx_in_valid]
        # 映射回原id
        neighbor_ids = df.loc[valid_indices].iloc[neighbor_positions]['id'].tolist()
        df.at[idx, dist_name] = neighbor_ids

方案二:用Geopandas空间索引(直观地理数据处理)

如果熟悉地理数据操作,Geopandas的空间索引可直接用公里作为阈值查询,代码更易读。

步骤:

  • 将DataFrame转换为GeoDataFrame,创建点几何对象
  • 转换为投影坐标系(支持米为单位,减少距离计算误差)
  • 构建空间索引,批量查询每个点在不同距离内的邻居
  • 提取邻居id并生成结果列

代码示例:

import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

# 加载数据
df = pd.read_csv("coordinates.csv", na_values=None)

# 转换为GeoDataFrame(WGS84坐标系)
gdf = gpd.GeoDataFrame(
    df,
    geometry=gpd.points_from_xy(df.longitude, df.latitude),
    crs="EPSG:4326"
)
# 转换为UTM投影(示例用北京附近的EPSG:32633,需根据数据区域调整)
gdf = gdf.to_crs("EPSG:32633")

# 构建空间索引
sindex = gdf.sindex

# 距离阈值(单位:米)
thresholds = {
    '1km': 1000,
    '5km': 5000,
    '10km': 10000,
    '50km': 50000
}

# 初始化结果列
for col in thresholds.keys():
    gdf[col] = [[] for _ in range(len(gdf))]

# 遍历查询邻居
for idx, row in gdf.iterrows():
    if pd.isna(row.geometry):
        continue
    # 生成扩大后的边界框,缩小候选范围
    bbox = row.geometry.buffer(thresholds['50km']).bounds
    candidate_indices = list(sindex.intersection(bbox))
    # 排除自身
    candidate_indices = [i for i in candidate_indices if i != idx]
    candidates = gdf.iloc[candidate_indices]
    # 计算距离
    distances = candidates.geometry.distance(row.geometry)
    # 按阈值筛选id
    for dist_name, dist_m in thresholds.items():
        neighbor_ids = candidates[distances < dist_m]['id'].tolist()
        gdf.at[idx, dist_name] = neighbor_ids

# 转回普通DataFrame(可选)
df = pd.DataFrame(gdf.drop(columns='geometry'))

方案对比

  • KDTree方案:速度更快,适合10万+级别的超大数据量,需处理弧度转换,对地理坐标逻辑要求稍高
  • Geopandas方案:代码直观,支持复杂地理操作,适合中等数据量,需注意选择对应区域的投影坐标系

额外优化建议

  • 提前过滤缺失值:避免循环中重复判断空值,减少无效计算
  • 避免重复计算:KDTree和空间索引会自动跳过(i,j)与(j,i)的重复距离计算
  • 批量操作优先:尽量用numpy/Geopandas的向量化方法代替Python循环,降低开销

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 20:47:28