GeoPandas: Encontre o ponto mais próximo em outro dataframe

20

Eu tenho 2 geodataframes:

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

e quero encontrar o nome do ponto mais próximo em gpd2 para cada linha em 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

Eu tenho tentado fazer isso funcionar usando uma função lambda:

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

com

def min_dist(point, gpd2):

    geoseries = some_function()
    return geoseries
RedM
fonte
Este método funcionou para mim: stackoverflow.com/questions/37402046/… veja o link
Johnny Cheesecutter

Respostas:

16

Você pode usar diretamente a função Shapely Pontos mais próximos (as geometrias do GeoSeries são geometrias 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

Explicação

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)
gene
fonte
Algo não está funcionando para mim e não consigo descobrir. A função retorna um GeoSeries vazio, mesmo que a geometria seja sólida. Por exemplo: sample_point = gpd2.geometry.unary_union[400] / sample_point in gpd2.geometry Isso retorna True. gpd2.geometry == sample_point Isso sai todo falso.
robroc
Além do acima: gpd2.geometry.geom_equals(sample_point)funciona.
robroc
13

Se você possui grandes quadros de dados, descobri que scipyo .querymétodo de índice espacial cKDTree retorna resultados muito rápidos para pesquisas de vizinhos mais próximos. Como ele usa um índice espacial, é uma ordem de magnitude mais rápida do que percorrer o quadro de dados e encontrar o mínimo de todas as distâncias. Também é mais rápido do que usar o shapely's nearest_pointscom RTree (o método de índice espacial disponível via geopandas) porque o cKDTree permite que você vectorize sua pesquisa, enquanto o outro método não.

Aqui está uma função auxiliar que retornará a distância e o 'Nome' do vizinho mais próximo em gpd2cada ponto em gpd1. Assume que ambos os gdfs têm uma geometrycoluna (de pontos).

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)

E se você deseja encontrar o ponto mais próximo de um LineString, aqui está um exemplo completo de trabalho:

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
fonte
É possível fornecer também o ponto mais próximo da linha, usando este método? Por exemplo, para ajustar uma localização GPS à rua mais próxima.
Hyperknot 5/12/18
Essa resposta é incrível! No entanto, o código para os pontos mais próximos da linha produz um erro para mim. Parece que a distância correta da linha mais próxima é retornada para cada ponto, mas a identificação da linha retornada está incorreta. Eu acho que é o cálculo idx, mas eu sou muito novo no Python, então não consigo entender o que é isso.
Shakedk 19/11
1

Descobri isso:

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

É claro que algumas críticas são bem-vindas. Eu não sou fã de recalcular gpd2 ['Dist'] para cada linha de gpd1 ...

RedM
fonte
1

A resposta de Gene não funcionou para mim. Finalmente, descobri que gpd2.geometry.unary_union resultou em uma geometria que continha apenas cerca de 30.000 do meu total de aproximadamente 150.000 pontos. Para qualquer outra pessoa com o mesmo problema, veja como eu o resolvi:

    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
fonte
0

Para quem tem erros de indexação com seus próprios dados enquanto usa a excelente resposta do @ JHuw , meu problema é que meus índices não estão alinhados. Redefinir o índice de gdfA e gdfB resolveu meus problemas, talvez isso possa ajudá-lo também @ 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
fonte