Repository: geopandas/geopandas

Issue:
DOC: sindex documentation missing
sindex apparently can be used separately as well. In general it would be good to have some documentation of it. https://geoffboeing.com/2016/10/r-tree-spatial-index-python/


Discussion:
I was literally about to make an issue for this, thank you @srenoes! @martinfleis feel free to assign this to me, I'll take care of it in the next couple of weeks.
@adriangb We’re in the middle of documentation restructuring now, so do not start working on it yet. I’ll keep you in mind once new structure is in place. 
Just tag me when/if you want me to work on this.

Resolving patch:
diff --git a/geopandas/base.py b/geopandas/base.py
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -2578,6 +2578,55 @@ def total_bounds(self):
 
     @property
     def sindex(self):
+        """Generate the spatial index
+
+        Creates R-tree spatial index based on ``pygeos.STRtree`` or
+        ``rtree.index.Index``.
+
+        Note that the  spatial index may not be fully
+        initialized until the first use.
+
+        Examples
+        --------
+        >>> from shapely.geometry import box
+        >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(5), range(5)))
+        >>> s
+        0    POINT (0.00000 0.00000)
+        1    POINT (1.00000 1.00000)
+        2    POINT (2.00000 2.00000)
+        3    POINT (3.00000 3.00000)
+        4    POINT (4.00000 4.00000)
+        dtype: geometry
+
+        Query the spatial index with a single geometry based on the bounding box:
+
+        >>> s.sindex.query(box(1, 1, 3, 3))
+        array([1, 2, 3])
+
+        Query the spatial index with a single geometry based on the predicate:
+
+        >>> s.sindex.query(box(1, 1, 3, 3), predicate="contains")
+        array([2])
+
+        Query the spatial index with an array of geometries based on the bounding
+        box:
+
+        >>> s2 = geopandas.GeoSeries([box(1, 1, 3, 3), box(4, 4, 5, 5)])
+        >>> s2
+        0    POLYGON ((3.00000 1.00000, 3.00000 3.00000, 1....
+        1    POLYGON ((5.00000 4.00000, 5.00000 5.00000, 4....
+        dtype: geometry
+
+        >>> s.sindex.query_bulk(s2)
+        array([[0, 0, 0, 1],
+               [1, 2, 3, 4]])
+
+        Query the spatial index with an array of geometries based on the predicate:
+
+        >>> s.sindex.query_bulk(s2, predicate="contains")
+        array([[0],
+               [2]])
+        """
         return self.geometry.values.sindex
 
     @property
diff --git a/geopandas/sindex.py b/geopandas/sindex.py
--- a/geopandas/sindex.py
+++ b/geopandas/sindex.py
@@ -82,6 +82,14 @@ def valid_query_predicates(self):
             -------
             set
                 Set of valid predicates for this spatial index.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries([Point(0, 0), Point(1, 1)])
+            >>> s.sindex.valid_query_predicates  # doctest: +SKIP
+            {'contains', 'crosses', 'intersects', 'within', 'touches', \
+'overlaps', None, 'covers', 'contains_properly'}
             """
             return {
                 None,
@@ -96,7 +104,10 @@ def valid_query_predicates(self):
             }
 
         def query(self, geometry, predicate=None, sort=False):
-            """Compatibility layer for pygeos.query.
+            """Return the index of all geometries in the tree with extents that
+            intersect the envelope of the input geometry.
+
+            Compatibility layer for pygeos-based ``sindex.query``.
 
             This is not a vectorized function, if speed is important,
             please use PyGEOS.
@@ -121,6 +132,29 @@ def query(self, geometry, predicate=None, sort=False):
             -------
             matches : ndarray of shape (n_results, )
                 Integer indices for matching geometries from the spatial index.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.query(box(1, 1, 3, 3))
+            array([1, 2, 3])
+
+            >>> s.sindex.query(box(1, 1, 3, 3), predicate="contains")
+            array([2])
             """
 
             # handle invalid predicates
@@ -202,11 +236,16 @@ def query(self, geometry, predicate=None, sort=False):
             return np.array(tree_idx, dtype=np.intp)
 
         def query_bulk(self, geometry, predicate=None, sort=False):
-            """Compatibility layer for pygeos.query_bulk.
+            """
+            Returns all combinations of each input geometry and geometries in
+            the tree where the envelope of each input geometry intersects with
+            the envelope of a tree geometry.
 
-            Iterates over `geometry` and queries index.
+            Compatibility layer for pygeos-based ``sindex.query_bulk``.
+
+            Iterates over ``geometry`` and queries index.
             This operation is not vectorized and may be slow.
-            Use PyGEOS with `query_bulk` for speed.
+            Use PyGEOS with ``query_bulk`` for speed.
 
             Parameters
             ----------
@@ -230,6 +269,36 @@ def query_bulk(self, geometry, predicate=None, sort=False):
             ndarray with shape (2, n)
                 The first subarray contains input geometry integer indexes.
                 The second subarray contains tree geometry integer indexes.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+            >>> s2 = geopandas.GeoSeries([box(2, 2, 4, 4), box(5, 5, 6, 6)])
+            >>> s2
+            0    POLYGON ((4.00000 2.00000, 4.00000 4.00000, 2....
+            1    POLYGON ((6.00000 5.00000, 6.00000 6.00000, 5....
+            dtype: geometry
+
+            >>> s.sindex.query_bulk(s2)
+            array([[0, 0, 0, 1, 1],
+                   [2, 3, 4, 5, 6]])
+
+            >>> s.sindex.query_bulk(s2, predicate="contains")
+            array([[0],
+                   [3]])
             """
             # Iterates over geometry, applying func.
             tree_index = []
@@ -249,11 +318,61 @@ def intersection(self, coordinates):
             coordinates : sequence or array
                 Sequence of the form (min_x, min_y, max_x, max_y)
                 to query a rectangle or (x, y) to query a point.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.intersection(box(1, 1, 3, 3).bounds)
+            array([1, 2, 3])
+
+            Alternatively, you can use ``query``:
+
+            >>> s.sindex.query(box(1, 1, 3, 3))
+            array([1, 2, 3])
+
             """
             return super().intersection(coordinates, objects=False)
 
         @property
         def size(self):
+            """Size of the spatial index
+
+            Number of leaves (input geometries) in the index.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.size
+            10
+            """
             if hasattr(self, "_size"):
                 size = self._size
             else:
@@ -266,6 +385,32 @@ def size(self):
 
         @property
         def is_empty(self):
+            """Check if the spatial index is empty
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.is_empty
+            False
+
+            >>> s2 = geopandas.GeoSeries()
+            >>> s2.sindex.is_empty
+            True
+            """
             return self.geometries.size == 0 or self.size == 0
 
         def __len__(self):
@@ -302,17 +447,29 @@ def __init__(self, geometry):
 
         @property
         def valid_query_predicates(self):
-            """Returns valid predicates for this spatial index.
+            """Returns valid predicates for the used spatial index.
 
             Returns
             -------
             set
                 Set of valid predicates for this spatial index.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries([Point(0, 0), Point(1, 1)])
+            >>> s.sindex.valid_query_predicates  # doctest: +SKIP
+            {'contains', 'crosses', 'covered_by', None, 'intersects', 'within', \
+'touches', 'overlaps', 'contains_properly', 'covers'}
             """
             return pygeos.strtree.VALID_PREDICATES | set([None])
 
         def query(self, geometry, predicate=None, sort=False):
-            """Wrapper for pygeos.query.
+            """
+            Return the index of all geometries in the tree with extents
+            that intersect the envelope of the input geometry.
+
+            Wrapper for pygeos.STRtree.query.
 
             This also ensures a deterministic (sorted) order for the results.
 
@@ -334,9 +491,32 @@ def query(self, geometry, predicate=None, sort=False):
             matches : ndarray of shape (n_results, )
                 Integer indices for matching geometries from the spatial index.
 
-            See also
-            --------
+            Notes
+            -----
             See PyGEOS.strtree documentation for more information.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.query(box(1, 1, 3, 3))
+            array([1, 2, 3])
+
+            >>> s.sindex.query(box(1, 1, 3, 3), predicate="contains")
+            array([2])
             """
 
             if predicate not in self.valid_query_predicates:
@@ -358,7 +538,19 @@ def query(self, geometry, predicate=None, sort=False):
             return matches
 
         def query_bulk(self, geometry, predicate=None, sort=False):
-            """Wrapper to expose underlaying pygeos objects to pygeos.query_bulk.
+            """
+            Returns all combinations of each input geometry and geometries in
+            the tree where the envelope of each input geometry intersects with
+            the envelope of a tree geometry.
+
+            Wrapper to expose underlaying pygeos objects to pygeos.query_bulk.
+
+            In the context of a spatial join, input geometries are the “left”
+            geometries that determine the order of the results, and tree geometries
+            are “right” geometries that are joined against the left geometries.
+            This effectively performs an inner join, where only those combinations
+            of geometries that can be joined based on envelope overlap or optional
+            predicate are returned.
 
             This also allows a deterministic (sorted) order for the results.
 
@@ -385,9 +577,39 @@ def query_bulk(self, geometry, predicate=None, sort=False):
                 The first subarray contains input geometry integer indexes.
                 The second subarray contains tree geometry integer indexes.
 
-            See also
-            --------
+            Notes
+            -----
             See PyGEOS.strtree documentation for more information.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+            >>> s2 = geopandas.GeoSeries([box(2, 2, 4, 4), box(5, 5, 6, 6)])
+            >>> s2
+            0    POLYGON ((4.00000 2.00000, 4.00000 4.00000, 2....
+            1    POLYGON ((6.00000 5.00000, 6.00000 6.00000, 5....
+            dtype: geometry
+
+            >>> s.sindex.query_bulk(s2)
+            array([[0, 0, 0, 1, 1],
+                   [2, 3, 4, 5, 6]])
+
+            >>> s.sindex.query_bulk(s2, predicate="contains")
+            array([[0],
+                   [3]])
             """
 
             if predicate not in self.valid_query_predicates:
@@ -416,11 +638,37 @@ def query_bulk(self, geometry, predicate=None, sort=False):
         def intersection(self, coordinates):
             """Wrapper for pygeos.query that uses the RTree API.
 
+            Compatibility wrapper, use ``query`` instead.
+
             Parameters
             ----------
             coordinates : sequence or array
                 Sequence of the form (min_x, min_y, max_x, max_y)
                 to query a rectangle or (x, y) to query a point.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point, box
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.query(box(1, 1, 3, 3))
+            array([1, 2, 3])
+
+            >>> s.sindex.intersection(box(1, 1, 3, 3).bounds)
+            array([1, 2, 3])
+
             """
             # convert bounds to geometry
             # the old API uses tuples of bound, but pygeos uses geometries
@@ -452,8 +700,58 @@ def intersection(self, coordinates):
 
         @property
         def size(self):
+            """Size of the spatial index
+
+            Number of leaves (input geometries) in the index.
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.size
+            10
+            """
             return len(self)
 
         @property
         def is_empty(self):
+            """Check if the spatial index is empty
+
+            Examples
+            --------
+            >>> from shapely.geometry import Point
+            >>> s = geopandas.GeoSeries(geopandas.points_from_xy(range(10), range(10)))
+            >>> s
+            0    POINT (0.00000 0.00000)
+            1    POINT (1.00000 1.00000)
+            2    POINT (2.00000 2.00000)
+            3    POINT (3.00000 3.00000)
+            4    POINT (4.00000 4.00000)
+            5    POINT (5.00000 5.00000)
+            6    POINT (6.00000 6.00000)
+            7    POINT (7.00000 7.00000)
+            8    POINT (8.00000 8.00000)
+            9    POINT (9.00000 9.00000)
+            dtype: geometry
+
+            >>> s.sindex.is_empty
+            False
+
+            >>> s2 = geopandas.GeoSeries()
+            >>> s2.sindex.is_empty
+            True
+            """
             return len(self) == 0


Test patch: