3

I have a large, static list of Shapely Points that I want to check against a large list of Shapely Polygons, filtering out any polygons that contain one or more of the points.

The polygons do not overlap one another.

The same list of points is checked against all polygons.

A simplified example of the overall shape of the data is pictured below. The real data is magnified to a much larger extent.

enter image description here

Currently, I am using the Python any and the Shapely contains functions:

from shapely.geometry.point import Point
from shapely.geometry.polygon import Polygon

# A large list of thousands of points
points: list[Point] = []

# A large list of thousands of polygons
polygons: list[Polygon] = []
...

# Want to speed up this loop, which can take many seconds to complete.
# Retain polygons that do not contain any points.
retained_polygons: list[Polygon] = []
for polygon in polygons:
    if not any(polygon.contains(point) for point in points):
        retained_polygons.append(polygon)

return retained_polygons

For performance reasons, we were hoping there might be a native method within Shapely that could do this more efficiently. We have thousands of polygons to check against thousands of points, and have profiled slow down to this part of the code.

8
  • Point-in-polygon performance correlates to the number of polygon vertices (smaller is better). Regularizing the polygons by unioning it with a fishnet might get you all you really need (though you do have to handle point-ON-line cases more robustly).
    – Vince
    Commented Aug 12 at 21:40
  • 2
    You need to add way more detail to your question. For example are the points stacked, do you have overlapping polygons, can points exist in multiple polygons, Is your data multipart, what's the source data format, do they have spatial indexes, coordinate system... Screen shots of your data is also helpful.
    – Hornbydd
    Commented Aug 12 at 22:54
  • If you loop through points first and use point.within against all polygons (instead of looping through polygons and using polygon.contains against all points) you could use a spatial index to narrow the number of polys to check e.g. toblerity.org/rtree/tutorial.html gis.stackexchange.com/a/103066/2856
    – user2856
    Commented Aug 12 at 23:47
  • Is it the same list of points you want to check with all the polygons or a seperate list of points per polygon?
    – Pieter
    Commented Aug 13 at 6:33
  • Do you mind adding geopandas as a dependency? You won't really need it, but depending on you clarifications it might save you some plumbing code to use indexes,...
    – Pieter
    Commented Aug 13 at 6:35

1 Answer 1

7

This is a typical case to use a spatial index.

Shapely has support for spatial indexes via shapely.STRtree.query

I did a quick test with 1000 polygons and 750 points derived of them... and even with those relatively small numbers using the rtree index speeds it up by a factor 1000. The larger the number... the bigger the gain.

  • Retained 499 polygons with tree, took 0:00:00.006998
  • Retained 499 polygons with loop, took 0:00:08.774927

Sample script:

import time
import geopandas as gpd
import numpy as np
import shapely
from shapely import Polygon


# Load some polygons as test data
poly_path = r"C:\temp\file.gpkg"
polygons_gdf = gpd.read_file(poly_path, columns=[], rows=1000, engine="pyogrio")
polygons = polygons_gdf.geometry.tolist()

# For the first half of the polygons, use the centroid as points
points = (polygons_gdf.loc[: len(polygons) / 2].geometry.centroid).tolist()
# For the first quarter of the polygons, add the representative_point as points as well,
# so we have multiple points for those polygons
points2 = polygons_gdf.loc[: len(polygons) / 2].geometry.representative_point().tolist()
points.extend(points2)

# Test the performance using spatial index
start = time.perf_counter()
polygons_tree = shapely.STRtree(polygons)
result = polygons_tree.query(points, predicate="intersects")
mask = np.zeros(len(polygons), dtype=bool)
mask[np.unique(result[1])] = True
retained_polygons_tree = polygons_tree.geometries[~mask]
elapsed = time.perf_counter() - start
print(f"Retained {len(retained_polygons_tree)} polygons with tree, took {elapsed:.6f}")

# Test the performance using a simple loop
start = time.perf_counter()
retained_polygons: list[Polygon] = []
for polygon in polygons:
    if not any(polygon.contains(point) for point in points):
        retained_polygons.append(polygon)
elapsed = time.perf_counter() - start
print(f"Retained {len(retained_polygons)} polygons with loop, took {elapsed:.6f}")

# Make sure the results are the same
assert all(retained_polygons_tree == retained_polygons)
2
  • Thanks @Pieter! I realized that my question's sample code did not match the question's description (missing the not in if not any(..)). I've updated the question's sample code so that it's consistent. Can this answer be updated as well to match?
    – johnthagen
    Commented Aug 13 at 17:43
  • Sure, I inverted the result
    – Pieter
    Commented Aug 13 at 18:45

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.