Repository: geopandas/geopandas

Issue:
BUG: set_geometry fails to set active geometry on df with MultiIndex of 3 levels and more
I noticed an issue one with `set_geometry`. This reproducer is with #2088. Two levels of MultiIndex are fine, three break.

```py
crs = "EPSG:4326"
df = pd.DataFrame(
    [[1, 0], [0, 1]],
    columns=[
        ["foo", "foo"],
        ["location", "location"],
        ["x", "y"],
    ],
)

x_col = df["foo", "location", "x"]
y_col = df["foo", "location", "y"]
df["geometry"] = gpd.points_from_xy(x_col, y_col)
```

With this df, using `set_geometry("geometry")` upgrades the df to GeoDataFrame but silently fails to set an active geometry column.
```py
df.set_geometry("geometry").crs

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/Git/geopandas/geopandas/geodataframe.py:439, in GeoDataFrame.crs(self)
    438 try:
--> 439     return self.geometry.crs
    440 except AttributeError:

File ~/mambaforge/envs/geopandas_dev/lib/python3.11/site-packages/pandas/core/generic.py:5902, in NDFrame.__getattr__(self, name)
   5901     return self[name]
-> 5902 return object.__getattribute__(self, name)

AttributeError: 'Series' object has no attribute 'crs'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
Cell In[37], line 1
----> 1 df.set_geometry("geometry").crs

File ~/mambaforge/envs/geopandas_dev/lib/python3.11/site-packages/pandas/core/generic.py:5902, in NDFrame.__getattr__(self, name)
   5895 if (
   5896     name not in self._internal_names_set
   5897     and name not in self._metadata
   5898     and name not in self._accessors
   5899     and self._info_axis._can_hold_identifiers_and_holds_name(name)
   5900 ):
   5901     return self[name]
-> 5902 return object.__getattribute__(self, name)

File ~/Git/geopandas/geopandas/geodataframe.py:441, in GeoDataFrame.crs(self)
    439     return self.geometry.crs
    440 except AttributeError:
--> 441     raise AttributeError(
    442         "The CRS attribute of a GeoDataFrame without an active "
    443         "geometry column is not defined. Use GeoDataFrame.set_geometry "
    444         "to set the active geometry column."
    445     )

AttributeError: The CRS attribute of a GeoDataFrame without an active geometry column is not defined. Use GeoDataFrame.set_geometry to set the active geometry column.
```

I would expect this to work since when you do `df["geometry"]` you get the Series back.

But I'll move it to a separate issue, so we can merge this.

_Originally posted by @martinfleis in https://github.com/geopandas/geopandas/issues/2088#issuecomment-1447912628_
            


Discussion:

Resolving patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1444,6 +1444,18 @@ def __getitem__(self, key):
         return a GeoDataFrame.
         """
         result = super().__getitem__(key)
+        # Custom logic to avoid waiting for pandas GH51895
+        # result is not geometry dtype for multi-indexes
+        if (
+            pd.api.types.is_scalar(key)
+            and key == ""
+            and isinstance(self.columns, pd.MultiIndex)
+            and isinstance(result, Series)
+            and not is_geometry_type(result)
+        ):
+            loc = self.columns.get_loc(key)
+            # squeeze stops multilevel columns from returning a gdf
+            result = self.iloc[:, loc].squeeze(axis="columns")
         geo_col = self._geometry_column_name
         if isinstance(result, Series) and isinstance(result.dtype, GeometryDtype):
             result.__class__ = GeoSeries


Test patch:
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1299,6 +1299,70 @@ def test_multiindex_with_geometry_label(self, dtype):
         result = gdf[["geometry"]]
         assert_frame_equal(result, gdf if dtype == "geometry" else pd.DataFrame(gdf))
 
+    def test_multiindex_geometry_colname_2_level(self):
+        # GH1763 https://github.com/geopandas/geopandas/issues/1763
+        crs = "EPSG:4326"
+        df = pd.DataFrame(
+            [[1, 0], [0, 1]], columns=[["location", "location"], ["x", "y"]]
+        )
+        x_col = df["location", "x"]
+        y_col = df["location", "y"]
+
+        gdf = GeoDataFrame(df, crs=crs, geometry=points_from_xy(x_col, y_col))
+        assert gdf.crs == crs
+        assert gdf.geometry.crs == crs
+        assert gdf.geometry.dtype == "geometry"
+        assert gdf._geometry_column_name == "geometry"
+        assert gdf.geometry.name == "geometry"
+
+    def test_multiindex_geometry_colname_3_level(self):
+        # GH1763 https://github.com/geopandas/geopandas/issues/1763
+        # Note 3-level case uses different code paths in pandas, it is not redundant
+        crs = "EPSG:4326"
+        df = pd.DataFrame(
+            [[1, 0], [0, 1]],
+            columns=[
+                ["foo", "foo"],
+                ["location", "location"],
+                ["x", "y"],
+            ],
+        )
+
+        x_col = df["foo", "location", "x"]
+        y_col = df["foo", "location", "y"]
+
+        gdf = GeoDataFrame(df, crs=crs, geometry=points_from_xy(x_col, y_col))
+        assert gdf.crs == crs
+        assert gdf.geometry.crs == crs
+        assert gdf.geometry.dtype == "geometry"
+        assert gdf._geometry_column_name == "geometry"
+        assert gdf.geometry.name == "geometry"
+
+    def test_multiindex_geometry_colname_3_level_new_col(self):
+        crs = "EPSG:4326"
+        df = pd.DataFrame(
+            [[1, 0], [0, 1]],
+            columns=[
+                ["foo", "foo"],
+                ["location", "location"],
+                ["x", "y"],
+            ],
+        )
+
+        x_col = df["foo", "location", "x"]
+        y_col = df["foo", "location", "y"]
+        df["geometry"] = GeoSeries.from_xy(x_col, y_col)
+        df2 = df.copy()
+        gdf = df.set_geometry("geometry", crs=crs)
+        assert gdf.crs == crs
+        assert gdf._geometry_column_name == "geometry"
+        assert gdf.geometry.name == "geometry"
+        # test again setting with tuple col name
+        gdf = df2.set_geometry(("geometry", "", ""), crs=crs)
+        assert gdf.crs == crs
+        assert gdf._geometry_column_name == ("geometry", "", "")
+        assert gdf.geometry.name == ("geometry", "", "")
+
 
 def test_geodataframe_crs():
     gdf = GeoDataFrame(columns=["geometry"])