Repository: geopandas/geopandas
Pull request #2621 (2022-10-28 21:58:40)

ENH: add minimum_bounding_circle as GeoSeries method
Expose `minimum_bounding_circle` as a GeoSeries method.

xref #2010


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0235b8e..8bd3c4e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Development version
 
+New features and improvements:
+
+- Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataframe (#2621).
+
 Deprecations and compatibility notes:
 
 - Added warning that ``unary_union`` will return ``'GEOMETRYCOLLECTION EMPTY'`` instead
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index bf7f4ec2..7d7317b9 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -86,6 +86,7 @@ Constructive methods and attributes
    GeoSeries.centroid
    GeoSeries.convex_hull
    GeoSeries.envelope
+   GeoSeries.minimum_bounding_circle
    GeoSeries.simplify
    GeoSeries.normalize
 
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index b67cf7e8..36408804 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -677,6 +677,18 @@ def representative_point(data):
         return out
 
 
+def minimum_bounding_circle(data):
+    if compat.USE_SHAPELY_20:
+        return shapely.minimum_bounding_circle(data)
+    elif compat.USE_PYGEOS:
+        return pygeos.minimum_bounding_circle(data)
+    else:
+        raise NotImplementedError(
+            f"shapely >= 2.0 or PyGEOS is required, "
+            f"version {shapely.__version__} is installed"
+        )
+
+
 #
 # Binary predicates
 #
diff --git a/geopandas/array.py b/geopandas/array.py
index 5f472333..a80c0214 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -511,6 +511,11 @@ class GeometryArray(ExtensionArray):
     def representative_point(self):
         return GeometryArray(vectorized.representative_point(self.data), crs=self.crs)
 
+    def minimum_bounding_circle(self):
+        return GeometryArray(
+            vectorized.minimum_bounding_circle(self.data), crs=self.crs
+        )
+
     def normalize(self):
         return GeometryArray(vectorized.normalize(self.data), crs=self.crs)
 
diff --git a/geopandas/base.py b/geopandas/base.py
index ada4c79d..b4c6a908 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -692,6 +692,39 @@ GeometryCollection
         """
         return _delegate_geo_method("representative_point", self)
 
+    def minimum_bounding_circle(self):
+        """Returns a ``GeoSeries`` of geometries representing the minimum bounding
+        circle that encloses each geometry.
+
+        Examples
+        --------
+
+        >>> from shapely.geometry import Polygon, LineString, Point
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
+        ...         LineString([(0, 0), (1, 1), (1, 0)]),
+        ...         Point(0, 0),
+        ...     ]
+        ... )
+        >>> s
+        0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
+        1    LINESTRING (0.00000 0.00000, 1.00000 1.00000, ...
+        2                              POINT (0.00000 0.00000)
+        dtype: geometry
+
+        >>> s.minimum_bounding_circle()
+        0    POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1....
+        1    POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1....
+        2                              POINT (0.00000 0.00000)
+        dtype: geometry
+
+        See also
+        --------
+        GeoSeries.convex_hull : convex hull geometry
+        """
+        return _delegate_geo_method("minimum_bounding_circle", self)
+
     def normalize(self):
         """Returns a ``GeoSeries`` of normalized
         geometries to normal form (or canonical form).


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 97c14209..eccd5c00 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -928,6 +928,21 @@ class TestGeomMethods:
         assert isinstance(e, GeoSeries)
         assert self.g3.crs == e.crs
 
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="minimum_bounding_circle is only implemented for pygeos, not shapely",
+    )
+    def test_minimum_bounding_circle(self):
+        mbc = self.g1.minimum_bounding_circle()
+        centers = GeoSeries([Point(0.5, 0.5)] * 2)
+        assert np.all(mbc.centroid.geom_equals_exact(centers, 0.001))
+        assert_series_equal(
+            mbc.area,
+            Series([1.560723, 1.560723]),
+        )
+        assert isinstance(mbc, GeoSeries)
+        assert self.g1.crs == mbc.crs
+
     def test_total_bounds(self):
         bbox = self.sol.x, self.sol.y, self.esb.x, self.esb.y
         assert isinstance(self.landmarks.total_bounds, np.ndarray)