Repository: geopandas/geopandas

Issue:
Spatial join error after using `explode()`
@jorisvandenbossche There is a specific situation that produces an error with `.sjoin()`. 

I receive a following error: 
```
  File "C:\Hyapp\Anaconda3\lib\site-packages\pandas\core\indexes\base.py", line 1330, in set_names
    raise TypeError("Must pass list-like as `names`.")

TypeError: Must pass list-like as `names`.
```

I was able to narrow this down, and the error only happens after using `df.explode()` function to turn MultiPolygons into multiple Polygons. 

Here is an example to produce the error using some data from OSM: 
```
# Test to produce an error
import osmnx as ox
import geopandas as gpd
buildings = ox.footprints_from_place("Punavuori, Finland")
buildings = buildings.explode()
landuse = ox.footprints_from_place("Punavuori, Finland", 'landuse')
join = gpd.sjoin(buildings, landuse)
```

The solution for fixing this was to `reset_index()` after using the `.explode()`. I am not sure if this is a bug or expected behavior, but wanted to report my findings anyway. 🙂 



Discussion:
Thanks for the reproducible example! 
The `explode` method results in a geodataframe with a MultiIndex, and this seems to result in that failure. So resetting the index is indeed a good workaround for now. We should look in general at how MultiIndex is supported (or not) in spatial join.


Resolving patch:
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -90,12 +90,27 @@ def sjoin(
     # and store references to the original indices, to be reaffixed later.
     # GH 352
     left_df = left_df.copy(deep=True)
-    left_index_name = left_df.index.name
-    left_df.index = left_df.index.rename(index_left)
+    try:
+        left_index_name = left_df.index.name
+        left_df.index = left_df.index.rename(index_left)
+    except TypeError:
+        index_left = [
+            "index_%s" % lsuffix + str(l) for l, ix in enumerate(left_df.index.names)
+        ]
+        left_index_name = left_df.index.names
+        left_df.index = left_df.index.rename(index_left)
     left_df = left_df.reset_index()
+
     right_df = right_df.copy(deep=True)
-    right_index_name = right_df.index.name
-    right_df.index = right_df.index.rename(index_right)
+    try:
+        right_index_name = right_df.index.name
+        right_df.index = right_df.index.rename(index_right)
+    except TypeError:
+        index_right = [
+            "index_%s" % rsuffix + str(l) for l, ix in enumerate(right_df.index.names)
+        ]
+        right_index_name = right_df.index.names
+        right_df.index = right_df.index.rename(index_right)
     right_df = right_df.reset_index()
 
     if op == "within":
@@ -184,7 +199,10 @@ def find_contains(a1, a2):
             .set_index(index_left)
             .drop(["_key_right"], axis=1)
         )
-        joined.index.name = left_index_name
+        if isinstance(index_left, list):
+            joined.index.names = left_index_name
+        else:
+            joined.index.name = left_index_name
 
     elif how == "left":
         result = result.set_index("_key_left")
@@ -200,7 +218,10 @@ def find_contains(a1, a2):
             .set_index(index_left)
             .drop(["_key_right"], axis=1)
         )
-        joined.index.name = left_index_name
+        if isinstance(index_left, list):
+            joined.index.names = left_index_name
+        else:
+            joined.index.name = left_index_name
 
     else:  # how == 'right':
         joined = (
@@ -216,6 +237,9 @@ def find_contains(a1, a2):
             .set_index(index_right)
             .drop(["_key_left", "_key_right"], axis=1)
         )
-        joined.index.name = right_index_name
+        if isinstance(index_right, list):
+            joined.index.names = right_index_name
+        else:
+            joined.index.name = right_index_name
 
     return joined


Test patch:
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -45,6 +45,20 @@ def dfs(request):
         df1.index.name = "df1_ix"
         df2.index.name = "df2_ix"
 
+    if request.param == "multi-index":
+        i1 = ["a", "b", "c"]
+        i2 = ["d", "e", "f"]
+        df1 = df1.set_index([i1, i2])
+        df2 = df2.set_index([i2, i1])
+
+    if request.param == "named-multi-index":
+        i1 = ["a", "b", "c"]
+        i2 = ["d", "e", "f"]
+        df1 = df1.set_index([i1, i2])
+        df2 = df2.set_index([i2, i1])
+        df1.index.names = ["df1_ix1", "df1_ix2"]
+        df2.index.names = ["df2_ix1", "df2_ix2"]
+
     # construction expected frames
     expected = {}
 
@@ -85,7 +99,15 @@ def test_crs_mismatch(self, dfs):
             sjoin(df1, df2)
 
     @pytest.mark.parametrize(
-        "dfs", ["default-index", "string-index", "named-index"], indirect=True
+        "dfs",
+        [
+            "default-index",
+            "string-index",
+            "named-index",
+            "multi-index",
+            "named-multi-index",
+        ],
+        indirect=True,
     )
     @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
     def test_inner(self, op, dfs):
@@ -106,11 +128,29 @@ def test_inner(self, op, dfs):
         if index in ["default-index", "string-index"]:
             exp = exp.set_index("index_left")
             exp.index.name = None
+        if index == "multi-index":
+            exp = exp.set_index(["level_0_x", "level_1_x"]).rename(
+                columns={"level_0_y": "index_right0", "level_1_y": "index_right1"}
+            )
+            exp.index.names = df1.index.names
+        if index == "named-multi-index":
+            exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
+                columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
+            )
+            exp.index.names = df1.index.names
 
         assert_frame_equal(res, exp)
 
     @pytest.mark.parametrize(
-        "dfs", ["default-index", "string-index", "named-index"], indirect=True
+        "dfs",
+        [
+            "default-index",
+            "string-index",
+            "named-index",
+            "multi-index",
+            "named-multi-index",
+        ],
+        indirect=True,
     )
     @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
     def test_left(self, op, dfs):
@@ -122,6 +162,10 @@ def test_left(self, op, dfs):
             exp = expected[op].dropna(subset=["index_left"]).copy()
         elif index == "named-index":
             exp = expected[op].dropna(subset=["df1_ix"]).copy()
+        elif index == "multi-index":
+            exp = expected[op].dropna(subset=["level_0_x"]).copy()
+        elif index == "named-multi-index":
+            exp = expected[op].dropna(subset=["df1_ix1"]).copy()
         exp = exp.drop("geometry_y", axis=1).rename(columns={"geometry_x": "geometry"})
         exp["df1"] = exp["df1"].astype("int64")
         if index == "default-index":
@@ -134,6 +178,16 @@ def test_left(self, op, dfs):
         if index in ["default-index", "string-index"]:
             exp = exp.set_index("index_left")
             exp.index.name = None
+        if index == "multi-index":
+            exp = exp.set_index(["level_0_x", "level_1_x"]).rename(
+                columns={"level_0_y": "index_right0", "level_1_y": "index_right1"}
+            )
+            exp.index.names = df1.index.names
+        if index == "named-multi-index":
+            exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
+                columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
+            )
+            exp.index.names = df1.index.names
 
         assert_frame_equal(res, exp)
 
@@ -167,7 +221,15 @@ def test_sjoin_invalid_args(self, dfs):
             sjoin(df1, df2.geometry)
 
     @pytest.mark.parametrize(
-        "dfs", ["default-index", "string-index", "named-index"], indirect=True
+        "dfs",
+        [
+            "default-index",
+            "string-index",
+            "named-index",
+            "multi-index",
+            "named-multi-index",
+        ],
+        indirect=True,
     )
     @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
     def test_right(self, op, dfs):
@@ -179,6 +241,10 @@ def test_right(self, op, dfs):
             exp = expected[op].dropna(subset=["index_right"]).copy()
         elif index == "named-index":
             exp = expected[op].dropna(subset=["df2_ix"]).copy()
+        elif index == "multi-index":
+            exp = expected[op].dropna(subset=["level_0_y"]).copy()
+        elif index == "named-multi-index":
+            exp = expected[op].dropna(subset=["df2_ix1"]).copy()
         exp = exp.drop("geometry_x", axis=1).rename(columns={"geometry_y": "geometry"})
         exp["df2"] = exp["df2"].astype("int64")
         if index == "default-index":
@@ -191,6 +257,16 @@ def test_right(self, op, dfs):
             exp = exp.set_index("index_right")
             exp = exp.reindex(columns=res.columns)
             exp.index.name = None
+        if index == "multi-index":
+            exp = exp.set_index(["level_0_y", "level_1_y"]).rename(
+                columns={"level_0_x": "index_left0", "level_1_x": "index_left1"}
+            )
+            exp.index.names = df2.index.names
+        if index == "named-multi-index":
+            exp = exp.set_index(["df2_ix1", "df2_ix2"]).rename(
+                columns={"df1_ix1": "index_left0", "df1_ix2": "index_left1"}
+            )
+            exp.index.names = df2.index.names
 
         assert_frame_equal(res, exp, check_index_type=False)