GeoPandas: trouver le point le plus proche dans une autre trame de données

20

J'ai 2 géodonnées:

import geopandas as gpd
from shapely.geometry import Point
gpd1 = gpd.GeoDataFrame([['John',1,Point(1,1)],['Smith',1,Point(2,2)],['Soap',1,Point(0,2)]],columns=['Name','ID','geometry'])
gpd2 = gpd.GeoDataFrame([['Work',Point(0,1.1)],['Shops',Point(2.5,2)],['Home',Point(1,1.1)]],columns=['Place','geometry'])

et je veux trouver le nom du point le plus proche dans gpd2 pour chaque ligne dans gpd1:

desired_output = 

    Name  ID     geometry  Nearest
0   John   1  POINT (1 1)     Home
1  Smith   1  POINT (2 2)    Shops
2   Soap   1  POINT (0 2)     Work

J'ai essayé de faire fonctionner cela en utilisant une fonction lambda:

gpd1['Nearest'] = gpd1.apply(lambda row: min_dist(row.geometry,gpd2)['Place'] , axis=1)

avec

def min_dist(point, gpd2):

    geoseries = some_function()
    return geoseries
RedM
la source
Cette méthode a fonctionné pour moi: stackoverflow.com/questions/37402046/… regardez le lien
Johnny Cheesecutter

Réponses:

16

Vous pouvez utiliser directement la fonction Shapely Points les plus proches (les géométries des GeoSeries sont des géométries Shapely):

from shapely.ops import nearest_points
# unary union of the gpd2 geomtries 
pts3 = gpd2.geometry.unary_union
def near(point, pts=pts3):
     # find the nearest point and return the corresponding Place value
     nearest = gpd2.geometry == nearest_points(point, pts)[1]
     return gpd2[nearest].Place.get_values()[0]
gpd1['Nearest'] = gpd1.apply(lambda row: near(row.geometry), axis=1)
gpd1
    Name  ID     geometry  Nearest
0   John   1  POINT (1 1)     Home
1  Smith   1  POINT (2 2)    Shops
2   Soap   1  POINT (0 2)     Work

Explication

for i, row in gpd1.iterrows():
    print nearest_points(row.geometry, pts3)[0], nearest_points(row.geometry, pts3)[1]
 POINT (1 1) POINT (1 1.1)
 POINT (2 2) POINT (2.5 2)
 POINT (0 2) POINT (0 1.1)
gène
la source
Quelque chose ne fonctionne pas pour moi et je ne peux pas le comprendre. La fonction renvoie une GeoSeries vide même si la géométrie est solide. Par exemple: sample_point = gpd2.geometry.unary_union[400] / sample_point in gpd2.geometry Cela renvoie True. gpd2.geometry == sample_point Cela sort tout faux.
robroc
Ajout à ce qui précède: gpd2.geometry.geom_equals(sample_point)fonctionne.
robroc
13

Si vous avez de grandes trames de données, j'ai trouvé que scipyc'est l'indice spatial cKDTree.query renvoie des résultats très rapides pour les recherches de voisins les plus proches. Comme il utilise un index spatial, ses ordres de grandeur sont plus rapides que de parcourir la trame de données et de trouver ensuite le minimum de toutes les distances. C'est aussi plus rapide que d'utiliser des galbéesnearest_points galbées avec RTree (la méthode d'index spatial disponible via les géopandas) car cKDTree vous permet de vectoriser votre recherche alors que l'autre méthode ne le fait pas.

Voici une fonction d'assistance qui renverra la distance et le «nom» du plus proche voisin à gpd2partir de chaque point gpd1. Cela suppose que les deux gdfs ont une geometrycolonne (de points).

import geopandas as gpd
import numpy as np
import pandas as pd

from scipy.spatial import cKDTree
from shapely.geometry import Point

gpd1 = gpd.GeoDataFrame([['John', 1, Point(1, 1)], ['Smith', 1, Point(2, 2)],
                         ['Soap', 1, Point(0, 2)]],
                        columns=['Name', 'ID', 'geometry'])
gpd2 = gpd.GeoDataFrame([['Work', Point(0, 1.1)], ['Shops', Point(2.5, 2)],
                         ['Home', Point(1, 1.1)]],
                        columns=['Place', 'geometry'])

def ckdnearest(gdA, gdB):
    nA = np.array(list(zip(gdA.geometry.x, gdA.geometry.y)) )
    nB = np.array(list(zip(gdB.geometry.x, gdB.geometry.y)) )
    btree = cKDTree(nB)
    dist, idx = btree.query(nA, k=1)
    gdf = pd.concat(
        [gdA, gdB.loc[idx, gdB.columns != 'geometry'].reset_index(),
         pd.Series(dist, name='dist')], axis=1)
    return gdf

ckdnearest(gpd1, gpd2)

Et si vous souhaitez trouver le point le plus proche d'une ligne de chaîne, voici un exemple de travail complet:

import itertools
from operator import itemgetter

import geopandas as gpd
import numpy as np
import pandas as pd

from scipy.spatial import cKDTree
from shapely.geometry import Point, LineString

gpd1 = gpd.GeoDataFrame([['John', 1, Point(1, 1)],
                         ['Smith', 1, Point(2, 2)],
                         ['Soap', 1, Point(0, 2)]],
                        columns=['Name', 'ID', 'geometry'])
gpd2 = gpd.GeoDataFrame([['Work', LineString([Point(100, 0), Point(100, 1)])],
                         ['Shops', LineString([Point(101, 0), Point(101, 1), Point(102, 3)])],
                         ['Home',  LineString([Point(101, 0), Point(102, 1)])]],
                        columns=['Place', 'geometry'])


def ckdnearest(gdfA, gdfB, gdfB_cols=['Place']):
    A = np.concatenate(
        [np.array(geom.coords) for geom in gdfA.geometry.to_list()])
    B = [np.array(geom.coords) for geom in gdfB.geometry.to_list()]
    B_ix = tuple(itertools.chain.from_iterable(
        [itertools.repeat(i, x) for i, x in enumerate(list(map(len, B)))]))
    B = np.concatenate(B)
    ckd_tree = cKDTree(B)
    dist, idx = ckd_tree.query(A, k=1)
    idx = itemgetter(*idx)(B_ix)
    gdf = pd.concat(
        [gdfA, gdfB.loc[idx, gdfB_cols].reset_index(drop=True),
         pd.Series(dist, name='dist')], axis=1)
    return gdf

c = ckdnearest(gpd1, gpd2)
JHuw
la source
Est-il possible de donner également le point le plus proche sur la ligne en utilisant cette méthode? Par exemple, pour capturer un emplacement GPS dans la rue la plus proche.
hyperknot
Cette réponse est incroyable! Cependant, le code des points les plus proches de la ligne produit un bogue pour moi. Il semble que la distance correcte par rapport à la ligne la plus proche soit renvoyée pour chaque point, mais l'ID de ligne renvoyé est incorrect. Je pense que c'est le calcul de l'idx, mais je suis assez nouveau sur Python, donc je ne parviens pas à en faire le tour.
Shakedk
1

Deviner:

def min_dist(point, gpd2):
    gpd2['Dist'] = gpd2.apply(lambda row:  point.distance(row.geometry),axis=1)
    geoseries = gpd2.iloc[gpd2['Dist'].argmin()]
    return geoseries

Bien sûr, certaines critiques sont les bienvenues. Je ne suis pas fan du recalcul de gpd2 ['Dist'] pour chaque ligne de gpd1 ...

RedM
la source
1

La réponse de Gene n'a pas fonctionné pour moi. Enfin, j'ai découvert que gpd2.geometry.unary_union avait pour résultat une géométrie qui ne contenait qu'environ 30 000 de mon total d'environ 150 000 points. Pour toute autre personne rencontrant le même problème, voici comment je l'ai résolu:

    from shapely.ops import nearest_points
    from shapely.geometry import MultiPoint

    gpd2_pts_list = gpd2.geometry.tolist()
    gpd2_pts = MultiPoint(gpd2_pts_list)
    def nearest(point, gpd2_pts, gpd2=gpd2, geom_col='geometry', src_col='Place'):
         # find the nearest point
         nearest_point = nearest_points(point, gpd2_pts)[1]
         # return the corresponding value of the src_col of the nearest point
         value = gpd2[gpd2[geom_col] == nearest_point][src_col].get_values()[0]
         return value

    gpd1['Nearest'] = gpd1.apply(lambda x: nearest(x.geometry, gpd2_pts), axis=1)
Inske
la source
0

Pour quiconque ayant des erreurs d'indexation avec ses propres données lors de l'utilisation de l' excellente réponse de @ JHuw , mon problème était que mes index ne s'alignaient pas. Réinitialiser l'index de gdfA et gdfB a résolu mes problèmes, cela peut peut-être aussi vous aider @ Shakedk .

import itertools
from operator import itemgetter

import geopandas as gpd
import numpy as np
import pandas as pd

from scipy.spatial import cKDTree
from shapely.geometry import Point, LineString

gpd1 = gpd.GeoDataFrame([['John', 1, Point(1, 1)],
                         ['Smith', 1, Point(2, 2)],
                         ['Soap', 1, Point(0, 2)]],
                        columns=['Name', 'ID', 'geometry'])
gpd2 = gpd.GeoDataFrame([['Work', LineString([Point(100, 0), Point(100, 1)])],
                         ['Shops', LineString([Point(101, 0), Point(101, 1), Point(102, 3)])],
                         ['Home',  LineString([Point(101, 0), Point(102, 1)])]],
                        columns=['Place', 'geometry'])


def ckdnearest(gdfA, gdfB, gdfB_cols=['Place']):
    # resetting the index of gdfA and gdfB here.
    gdfA = gdfA.reset_index(drop=True)
    gdfB = gdfB.reset_index(drop=True)
    A = np.concatenate(
        [np.array(geom.coords) for geom in gdfA.geometry.to_list()])
    B = [np.array(geom.coords) for geom in gdfB.geometry.to_list()]
    B_ix = tuple(itertools.chain.from_iterable(
        [itertools.repeat(i, x) for i, x in enumerate(list(map(len, B)))]))
    B = np.concatenate(B)
    ckd_tree = cKDTree(B)
    dist, idx = ckd_tree.query(A, k=1)
    idx = itemgetter(*idx)(B_ix)
    gdf = pd.concat(
        [gdfA, gdfB.loc[idx, gdfB_cols].reset_index(drop=True),
         pd.Series(dist, name='dist')], axis=1)
    return gdf

c = ckdnearest(gpd1, gpd2)
Markus Rosenfelder
la source