GeoPandas: Finde den nächsten Punkt in einem anderen Datenrahmen


20

Ich habe 2 Geodatenrahmen:

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'])

und ich möchte den Namen des nächsten Punktes in gpd2 für jede Zeile in gpd1 finden:

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

Ich habe versucht, dies mit einer Lambda-Funktion zum Laufen zu bringen:

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

mit

def min_dist(point, gpd2):

    geoseries = some_function()
    return geoseries

Antworten:


16

Sie können die Shapely-Funktion Nearest points direkt verwenden (die Geometrien der GeoSeries sind Shapely-Geometrien):

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

Erläuterung

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)

Etwas funktioniert bei mir nicht und ich kann es nicht herausfinden. Die Funktion gibt eine leere GeoSeries zurück, obwohl die Geometrie fest ist. Zum Beispiel: sample_point = gpd2.geometry.unary_union[400] / sample_point in gpd2.geometry Dies gibt True zurück. gpd2.geometry == sample_point Das kommt alles falsch raus.
Robroc

Ergänzung zu oben: gpd2.geometry.geom_equals(sample_point)funktioniert.
Robroc

13

Wenn Sie über große Datenrahmen scipyverfügen, hat die räumliche Indexmethode cKDTree .querysehr schnelle Ergebnisse für die Suche nach nächsten Nachbarn geliefert . Da ein räumlicher Index verwendet wird, ist er um Größenordnungen schneller als das Durchlaufen des Datenrahmens und das Ermitteln des Minimums aller Entfernungen. Es ist auch schneller als die Verwendung von nearest_pointsShapelys mit RTree (der über Geopandas verfügbaren räumlichen Indexmethode), da Sie mit cKDTree Ihre Suche vektorisieren können, während dies mit der anderen Methode nicht möglich ist.

Hier ist eine Hilfsfunktion, die die Entfernung und den 'Namen' des nächsten Nachbarn in gpd2von jedem Punkt in zurückgibt gpd1. Es wird davon ausgegangen, dass beide gdfs eine geometrySpalte (von Punkten) haben.

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)

Und wenn Sie den nächstgelegenen Punkt zu einem LineString finden möchten, finden Sie hier ein voll funktionsfähiges Beispiel:

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)

Kann man mit dieser Methode auch den nächstgelegenen Punkt auf der Linie angeben? Zum Beispiel, um einen GPS-Standort auf der nächsten Straße abzufangen.
Hyperknot

Diese Antwort ist unglaublich! Der Code für die nächstgelegenen Punkte zur Linie erzeugt jedoch einen Fehler für mich. Es scheint, dass für jeden Punkt der richtige Abstand von der nächstgelegenen Linie zurückgegeben wird, aber die zurückgegebene Linien-ID ist falsch. Ich denke, es ist die Idx-Berechnung, aber ich bin ziemlich neu in Python, also kann ich es nicht schaffen, meinen Kopf darum zu wickeln.
Shakedk

1

Herausgefunden:

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

Kritik ist natürlich willkommen. Ich bin kein Fan davon, gpd2 ['Dist'] für jede Zeile von gpd1 neu zu berechnen ...


1

Die Antwort von Gene hat bei mir nicht funktioniert. Schließlich entdeckte ich, dass gpd2.geometry.unary_union zu einer Geometrie führte, die nur ungefähr 30.000 meiner insgesamt ungefähr 150.000 Punkte enthielt. Für alle anderen, die auf dasselbe Problem stoßen, habe ich Folgendes gelöst:

    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)

0

Für alle, die Indexierungsfehler mit ihren eigenen Daten haben, während sie die ausgezeichnete Antwort von @ JHuw verwenden , bestand mein Problem darin, dass meine Indizes nicht ausgerichtet wurden. Das Zurücksetzen des Index von gdfA und gdfB hat meine Probleme gelöst. Vielleicht hilft Ihnen das auch bei 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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.