From 5c8ae832e030228b5deec02588c4e1b78dee0f18 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 19 Jul 2022 00:47:12 +0200 Subject: [PATCH] REGR: fix recursion error in GeoDataFrame constructor with MultiIndex (#2497) * REGR: fix recursion error in GeoDataFrame constructor with MultiIndex * Add test for pivot (GH-2057) * add changelog Co-authored-by: Matt Richards <45483497+m-richards@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ geopandas/geodataframe.py | 6 +++++- geopandas/tests/test_geodataframe.py | 19 +++++++++++++++++++ geopandas/tests/test_op_output_types.py | 5 +++++ geopandas/tests/test_pandas_methods.py | 10 ++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9287f3..a3dc849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Deprecations and compatibility notes: Bug fixes: +- Fix regression (RecursionError) in reshape methods such as ``unstack()`` + and ``pivot()`` involving MultiIndex, or GeoDataFrame construction with + MultiIndex (#2486). + Notes on (optional) dependencies: Version 0.11 (June 20, 2022) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 816743e..92dc504 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -148,7 +148,11 @@ class GeoDataFrame(GeoPandasBase, DataFrame): if crs is not None and data.crs != crs: raise ValueError(crs_mismatch_error) - if geometry is None and "geometry" in self.columns: + if ( + geometry is None + and self.columns.nlevels == 1 + and "geometry" in self.columns + ): # Check for multiple columns with name "geometry". If there are, # self["geometry"] is a gdf and constructor gets recursively recalled # by pandas internals trying to access this diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py index a1a02ae..3cff510 100644 --- a/geopandas/tests/test_geodataframe.py +++ b/geopandas/tests/test_geodataframe.py @@ -1220,6 +1220,25 @@ class TestConstructor: with pytest.raises(ValueError): GeoDataFrame(df3, geometry="geom") + @pytest.mark.parametrize("dtype", ["geometry", "object"]) + def test_multiindex_with_geometry_label(self, dtype): + # DataFrame with MultiIndex where "geometry" label corresponds to + # multiple columns + df = pd.DataFrame([[Point(0, 0), Point(1, 1)], [Point(2, 2), Point(3, 3)]]) + df = df.astype(dtype) + df.columns = pd.MultiIndex.from_product([["geometry"], [0, 1]]) + # don't error in constructor + gdf = GeoDataFrame(df) + # Getting the .geometry column gives GeoDataFrame for both columns + # (but with first MultiIndex level removed) + # TODO should this give an error instead? + result = gdf.geometry + assert result.shape == gdf.shape + assert result.columns.tolist() == [0, 1] + assert_frame_equal(result, gdf["geometry"]) + result = gdf[["geometry"]] + assert_frame_equal(result, gdf if dtype == "geometry" else pd.DataFrame(gdf)) + def test_geodataframe_crs(): gdf = GeoDataFrame(columns=["geometry"]) diff --git a/geopandas/tests/test_op_output_types.py b/geopandas/tests/test_op_output_types.py index 3a4155c..5a447f3 100644 --- a/geopandas/tests/test_op_output_types.py +++ b/geopandas/tests/test_op_output_types.py @@ -289,6 +289,11 @@ def test_expanddim_in_unstack(): else: # pandas GH37369, unstack doesn't call finalize assert unstack._geometry_column_name == "geometry" + # https://github.com/geopandas/geopandas/issues/2486 + s.name = "geometry" + unstack = s.unstack() + assert_object(unstack, GeoDataFrame, None, None) + # indexing / constructor_sliced tests diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py index 252af1d..678b5a0 100644 --- a/geopandas/tests/test_pandas_methods.py +++ b/geopandas/tests/test_pandas_methods.py @@ -657,6 +657,16 @@ def test_df_apply_returning_series(df): assert_series_equal(result, df["value1"].rename(None)) +def test_pivot(df): + # https://github.com/geopandas/geopandas/issues/2057 + # pivot failing due to creating a MultiIndex + result = df.pivot(columns="value1") + expected = GeoDataFrame(pd.DataFrame(df).pivot(columns="value1")) + # TODO assert_geodataframe_equal crashes + assert isinstance(result, GeoDataFrame) + assert_frame_equal(result, expected) + + def test_preserve_attrs(df): # https://github.com/geopandas/geopandas/issues/1654 df.attrs["name"] = "my_name"