30

I am wondering whether it is possible to identify all neighbors to each polygon using only Python (with e.g. GeoPandas) in the same way as with Python in QGIS (Find neighbors polygon).

1

4 Answers 4

41

The following script finds and adds neighbors' names joined by comma as a new field value.

import geopandas as gpd

file = "C:/path/to/shapefile.shp"    

# open file
gdf = gpd.read_file(file)

# add NEIGHBORS column
gdf["NEIGHBORS"] = None  

for index, country in gdf.iterrows():   

    # get 'not disjoint' countries
    neighbors = gdf[~gdf.geometry.disjoint(country.geometry)].NAME.tolist()

    # remove own name of the country from the list
    neighbors = [ name for name in neighbors if country.NAME != name ]

    # add names of neighbors as NEIGHBORS value
    gdf.at[index, "NEIGHBORS"] = ", ".join(neighbors)
   
# save GeoDataFrame as a new file
gdf.to_file("c:/path/to/newfile.shp")

enter image description here

3
  • For more details also check this great answer, "iteration in Pandas is an anti-pattern and is something you should only do when you have exhausted every other option."
    – Taras
    Commented Dec 21, 2022 at 6:28
  • 1
    @Taras Yes, it is a great answer. If my English was good, I would like to give answers like that, but it would take a day (maybe days) to reply such answers in English. Commented Dec 21, 2022 at 9:23
  • No worries @_@ I probably know what you exactly mean, this is just a reference to keep in mind >_<
    – Taras
    Commented Dec 21, 2022 at 9:37
20

This is an addendum to @Kadir's answer (which works great).

For one, instead of using not disjoint you can just use touches directly, which does the same thing but is easier to read.

If, as the OP asked, you want to search internally to find all neighboring geometries in a single GeoDataFrame:

for index, row in df.iterrows():  
    neighbors = df[df.geometry.touches(row['geometry'])].name.tolist() 
    neighbors = neighbors.remove(row.name)
    df.at[index, "my_neighbors"] = ", ".join(neighbors)

For a related capability you may want to determine whether each row of a DataFrame borders some particular other shape, potentially from a different DataFrame or some input value (converted into a GeoDataFrame).

someSpecialGeometry = GEOdataframeRow['geometry'].values[0] 
df['isNeighbor'] = df.apply(lambda row: row['geometry'].touches(someSpecialGeometry), axis=1)

This creates a boolean-valued column for whether each row borders the shape of interest.

1
  • For more details also check this great answer, "iteration in Pandas is an anti-pattern and is something you should only do when you have exhausted every other option."
    – Taras
    Commented Dec 21, 2022 at 6:28
6

This is a further addendum relating to @Aaron's comment.

If you find 'touches' is failing to find all neighboring polygons like it should this is likely down to the resolution of your polygon borders which may in fact intersect hence 'touches' ignoring them.

I solved this issue using overlaps additionally to touches.

neighbors = np.array(df[df.geometry.touches(row['geometry'])].name)
#overlapping neighbors use if discrepances found with touches
overlap = np.array(df[df.geometry.overlaps(row['geometry'])].name)

neighbors = np.union1d(neighbors, overlap)
2

Another option is to spatial join the dataframe to itself, and groupby:

import geopandas as gpd
df = gpd.read_file(r"/home/bera/Desktop/gistest/world.shp")
df.plot(figsize=(20,10), column="NAME")

enter image description here

# df.columns
# Index(['fid', 'iso_a2', 'NAME', 'FIPS_10_', 'ISO_A3', 'WB_A2', 'WB_A3',
#        'geometry'],
#       dtype='object')

#Spatial join the dataframe to itself by predicate touching (=adjacent)
sj = df.sjoin(df[["NAME","geometry"]], how="left", predicate="touches")
sj[["NAME_left","NAME_right"]].head()
# Out[13]: 
#   NAME_left NAME_right
# 0    Norway     Sweden
# 0    Norway     Russia
# 0    Norway    Finland
# 1    Sweden     Norway
# 1    Sweden    Finland

#Groupby NAME_left column, list all neighbors, rename columns
sj = sj.groupby("NAME_left", as_index=False)["NAME_right"].apply(list).rename(columns={"NAME_left":"NAME", "NAME_right":"neighbors"})

sj.sample(5)
#            NAME                                          neighbors
# 168    Portugal                                            [Spain]
# 187    Slovakia       [Poland, Hungary, Austria, Czechia, Ukraine]
# 144     Namibia  [South Africa, Zimbabwe, Zambia, Botswana, Ang...
# 211        Togo                       [Ghana, Benin, Burkina Faso]
# 48   Costa Rica                                [Panama, Nicaragua]

sj.loc[sj["NAME"]=="Germany"]
#        NAME                                          neighbors
# 77  Germany  [France, Netherlands, Belgium, Poland, Denmark...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.