Repository: geopandas/geopandas
Pull request #3154 (2024-01-23 13:18:53)

ENH: expose relevant get_num_* functions as count_geometries and count_interior_rings
Exposing `get_num_geometries` as `count_geometries`, `get_num_interior_rings` as `count_interior_rings` and replacing the implementation of `count_coordinates` with `get_num_coordinates` to avoid the loop.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdd3e6a0..26f4696d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,9 @@ API changes:
 
 New methods:
 
+
+- Added `count_geometries` method from shapely to GeoSeries/GeoDataframe (#3154).
+- Added `count_interior_rings` method from shapely to GeoSeries/GeoDataframe (#3154)
 - Added `relate_pattern` method from shapely to GeoSeries/GeoDataframe (#3211).
 - Added `intersection_all` method from shapely to GeoSeries/GeoDataframe (#3228).
 - Added `line_merge` method from shapely to GeoSeries/GeoDataframe (#3214).
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index d17ef7c7..6c47e76e 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -35,6 +35,8 @@ General methods and attributes
    GeoSeries.z
    GeoSeries.get_coordinates
    GeoSeries.count_coordinates
+   GeoSeries.count_geometries
+   GeoSeries.count_interior_rings
    GeoSeries.set_precision
    GeoSeries.get_precision
 
diff --git a/geopandas/array.py b/geopandas/array.py
index d745712f..c63d714e 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -537,9 +537,13 @@ class GeometryArray(ExtensionArray):
         return shapely.length(self._data)
 
     def count_coordinates(self):
-        out = np.empty(len(self._data), dtype=np.int_)
-        out[:] = [shapely.count_coordinates(s) for s in self._data]
-        return out
+        return shapely.get_num_coordinates(self._data)
+
+    def count_geometries(self):
+        return shapely.get_num_geometries(self._data)
+
+    def count_interior_rings(self):
+        return shapely.get_num_interior_rings(self._data)
 
     def get_precision(self):
         return shapely.get_precision(self._data)
diff --git a/geopandas/base.py b/geopandas/base.py
index 40eab3c5..64470cbe 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -362,7 +362,7 @@ GeometryCollection
     def count_coordinates(self):
         """
         Returns a ``Series`` containing the count of the number of coordinate pairs
-        in a geometry array.
+        in each geometry.
 
         Examples
         --------
@@ -393,14 +393,104 @@ GeometryCollection
         2    1
         3    5
         4    0
-        dtype: int64
+        dtype: int32
 
         See also
         --------
         GeoSeries.get_coordinates : extract coordinates as a :class:`~pandas.DataFrame`
+        GoSeries.count_geometries : count the number of geometries in a collection
         """
         return Series(self.geometry.values.count_coordinates(), index=self.index)
 
+    def count_geometries(self):
+        """
+        Returns a ``Series`` containing the count of geometries in each multi-part
+        geometry.
+
+        For single-part geometry objects, this is always 1. For multi-part geometries,
+        like ``MultiPoint`` or ``MultiLineString``, it is the number of parts in the
+        geometry. For ``GeometryCollection``, it is the number of geometries direct
+        parts of the collection (the method does not recurse into collections within
+        collections).
+
+
+        Examples
+        --------
+        >>> from shapely.geometry import Point, MultiPoint, LineString, MultiLineString
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         MultiPoint([(0, 0), (1, 1), (1, -1), (0, 1)]),
+        ...         MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))]),
+        ...         LineString([(0, 0), (1, 1), (1, -1)]),
+        ...         Point(0, 0),
+        ...     ]
+        ... )
+        >>> s
+        0     MULTIPOINT ((0 0), (1 1), (1 -1), (0 1))
+        1    MULTILINESTRING ((0 0, 1 1), (-1 0, 1 0))
+        2                  LINESTRING (0 0, 1 1, 1 -1)
+        3                                  POINT (0 0)
+        dtype: geometry
+
+        >>> s.count_geometries()
+        0    4
+        1    2
+        2    1
+        3    1
+        dtype: int32
+
+        See also
+        --------
+        GeoSeries.count_coordinates : count the number of coordinates in a geometry
+        GeoSeries.count_interior_rings : count the number of interior rings
+        """
+        return Series(self.geometry.values.count_geometries(), index=self.index)
+
+    def count_interior_rings(self):
+        """
+        Returns a ``Series`` containing the count of the number of interior rings
+        in a polygonal geometry.
+
+        For non-polygonal geometries, this is always 0.
+
+        Examples
+        --------
+        >>> from shapely.geometry import Polygon, Point
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon(
+        ...             [(0, 0), (0, 5), (5, 5), (5, 0)],
+        ...             [[(1, 1), (1, 4), (4, 4), (4, 1)]],
+        ...         ),
+        ...         Polygon(
+        ...             [(0, 0), (0, 5), (5, 5), (5, 0)],
+        ...             [
+        ...                 [(1, 1), (1, 2), (2, 2), (2, 1)],
+        ...                 [(3, 2), (3, 3), (4, 3), (4, 2)],
+        ...             ],
+        ...         ),
+        ...         Point(0, 1),
+        ...     ]
+        ... )
+        >>> s
+        0    POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 4,...
+        1    POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0), (1 1, 1 2,...
+        2                                          POINT (0 1)
+        dtype: geometry
+
+        >>> s.count_interior_rings()
+        0    1
+        1    2
+        2    0
+        dtype: int32
+
+        See also
+        --------
+        GeoSeries.count_coordinates : count the number of coordinates in a geometry
+        GeoSeries.count_geometries : count the number of geometries in a collection
+        """
+        return Series(self.geometry.values.count_interior_rings(), index=self.index)
+
     @property
     def is_simple(self):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index e8af60de..d72fbb26 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -557,10 +557,46 @@ class TestGeomMethods:
 
     def test_count_coordinates(self):
         expected = Series(np.array([4, 5]), index=self.g1.index)
-        assert_series_equal(self.g1.count_coordinates(), expected)
+        assert_series_equal(self.g1.count_coordinates(), expected, check_dtype=False)
 
         expected = Series(np.array([4, 0]), index=self.na_none.index)
-        assert_series_equal(self.na_none.count_coordinates(), expected)
+        assert_series_equal(
+            self.na_none.count_coordinates(), expected, check_dtype=False
+        )
+
+    def test_count_geometries(self):
+        expected = Series(np.array([4, 2, 1, 1, 0]))
+        s = GeoSeries(
+            [
+                MultiPoint([(0, 0), (1, 1), (1, -1), (0, 1)]),
+                MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))]),
+                LineString([(0, 0), (1, 1), (1, -1)]),
+                Point(0, 0),
+                None,
+            ]
+        )
+        assert_series_equal(s.count_geometries(), expected, check_dtype=False)
+
+    def test_count_interior_rings(self):
+        expected = Series(np.array([1, 2, 0, 0]))
+        s = GeoSeries(
+            [
+                Polygon(
+                    [(0, 0), (0, 5), (5, 5), (5, 0)],
+                    [[(1, 1), (1, 4), (4, 4), (4, 1)]],
+                ),
+                Polygon(
+                    [(0, 0), (0, 5), (5, 5), (5, 0)],
+                    [
+                        [(1, 1), (1, 2), (2, 2), (2, 1)],
+                        [(3, 2), (3, 3), (4, 3), (4, 2)],
+                    ],
+                ),
+                Point(0, 1),
+                None,
+            ]
+        )
+        assert_series_equal(s.count_interior_rings(), expected, check_dtype=False)
 
     def test_crosses(self):
         expected = [False, False, False, False, False, False, False]