Repository: geopandas/geopandas

Issue:
BUG: GeoDataFrame.explode() breaks relationship between index and columns
- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest version of geopandas.

- [ ] (optional) I have confirmed this bug exists on the master branch of geopandas.

---

#### Simple example:

```python
from shapely.geometry import GeometryCollection, Point
import geopandas as gpd

geom = gpd.GeoSeries([GeometryCollection([Point((0, i)), 
                                          Point((1, i))]) for i in range(3)])
gdf = gpd.GeoDataFrame(geometry=geom, crs='EPSG:27700') 
gdf['id'] = [2, 9, 7]
gdf['x'] = [1, 2, 3]
gdf = gdf.set_index('id')
print(gdf)
x = gdf.explode(index_parts=True)
print(x)
```

Output:
```
                                             geometry  x
id                                                      
2   GEOMETRYCOLLECTION (POINT (0.000 0.000), POINT...  1
9   GEOMETRYCOLLECTION (POINT (0.000 1.000), POINT...  2
7   GEOMETRYCOLLECTION (POINT (0.000 2.000), POINT...  3

      x             geometry
id                          
2  0  1  POINT (0.000 0.000)
   1  1  POINT (1.000 0.000)
9  0  3  POINT (0.000 2.000)
   1  3  POINT (1.000 2.000)
7  0  2  POINT (0.000 1.000)
   1  2  POINT (1.000 1.000)
```
Note the values in column `x` in the rows where index level `id` is 9 or 7 in the exploded GeoDataFrame as compared to the original.

#### Problem description

In the GeoDataFrame returned by `explode` in the example above, I would expect values in rows produced by exploding a row `r`, to match the values in row `r` for each column apart from the geometry column. So, in the example above, I would expect the output of the second print statement to be:

```
      x             geometry
id                          
2  0  1  POINT (0.000 0.000)
   1  1  POINT (1.000 0.000)
9  0  2  POINT (0.000 2.000)
   1  2  POINT (1.000 2.000)
7  0  3  POINT (0.000 1.000)
   1  3  POINT (1.000 1.000)
```

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.9.5 (default, Nov 18 2021, 16:00:48)  [GCC 10.3.0]
executable : /home/william/venvs/std/bin/python3
machine    : Linux-5.15.5-76051505-generic-x86_64-with-glibc2.33

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : None
GEOS lib   : None
GDAL       : 3.3.0
GDAL data dir: /home/william/venvs/std/lib/python3.9/site-packages/fiona/gdal_data
PROJ       : 8.2.0
PROJ data dir: /home/william/venvs/std/lib/python3.9/site-packages/pyproj/proj_dir/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.10.2
pandas     : 1.3.4
fiona      : 1.8.20
numpy      : 1.21.4
shapely    : 1.8.0
rtree      : 0.9.7
pyproj     : 3.3.0
matplotlib : 3.5.0
mapclassify: None
geopy      : None
psycopg2   : None
geoalchemy2: None
pyarrow    : None
pygeos     : None

</details>

BUG: Unexpected behaviour with explode()
- [ ] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest version of geopandas.

- [ ] (optional) I have confirmed this bug exists on the master branch of geopandas.

---
```
import geopandas as gpd
from shapely.geometry import Polygon

index = [0, 0]
ids = ['one', 'two']
geometry = [Polygon([[0, 0], [0, 1], [1, 1]]),
           Polygon([[2, 2], [2, 3], [3, 3]])]

gdf = gpd.GeoDataFrame(ids, columns=['ids'], index=index, geometry=geometry)
gdf.explode(ignore_index=True)
```

Output:

![image](https://user-images.githubusercontent.com/30041765/142169095-ea6f7b61-91f9-4ce8-a163-61543c32134e.png)

#### Problem description

Is this the expected behaviour when indexes are duplicated? Or the expected output should be:

![image](https://user-images.githubusercontent.com/30041765/142170447-78fa1b30-fc5b-45e9-88fb-5ac44d2da74d.png)

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:15:42) [MSC v.1916 64 bit (AMD64)]
executable : C:\Users\skizoforme\Miniconda3\envs\geo\python.exe
machine    : Windows-10-10.0.19042-SP0

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : None
GEOS lib   : None
GDAL       : 3.2.1
GDAL data dir: None
PROJ       : 7.2.0
PROJ data dir: C:\Users\skizoforme\Miniconda3\envs\geo\Library\share\proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.10.2
pandas     : 1.3.4
fiona      : 1.8.18
numpy      : 1.21.4
shapely    : 1.7.1
rtree      : 0.9.7
pyproj     : 3.1.0
matplotlib : 3.4.3
mapclassify: 2.4.3
geopy      : None
psycopg2   : 2.8.6 (dt dec pq3 ext lo64)
geoalchemy2: None
pyarrow    : 3.0.0
pygeos     : 0.9

</details>



Discussion:

Thanks! I can confirm that this behaviour exists on master as well. It is a bug, the index should have no effect on a result of explode.

Resolving patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1674,43 +1674,26 @@ def explode(self, column=None, ignore_index=False, index_parts=None, **kwargs):
                 )
             index_parts = True
 
-        df_copy = self.copy()
+        exploded_geom = self.geometry.reset_index(drop=True).explode(index_parts=True)
 
-        level_str = f"level_{df_copy.index.nlevels}"
-
-        if level_str in df_copy.columns:  # GH1393
-            df_copy = df_copy.rename(columns={level_str: f"__{level_str}"})
-
-        if index_parts:
-            exploded_geom = df_copy.geometry.explode(index_parts=True)
-            exploded_index = exploded_geom.index
-            exploded_geom = exploded_geom.reset_index(level=-1, drop=True)
-        else:
-            exploded_geom = df_copy.geometry.explode(index_parts=True).reset_index(
-                level=-1, drop=True
-            )
-            exploded_index = exploded_geom.index
-
-        df = (
-            df_copy.drop(df_copy._geometry_column_name, axis=1)
-            .join(exploded_geom)
-            .set_geometry(self._geometry_column_name)
-            .__finalize__(self)
-        )
+        df = GeoDataFrame(
+            self.drop(self._geometry_column_name, axis=1).take(
+                exploded_geom.index.droplevel(-1)
+            ),
+            geometry=exploded_geom.values,
+        ).__finalize__(self)
 
         if ignore_index:
             df.reset_index(inplace=True, drop=True)
         elif index_parts:
             # reset to MultiIndex, otherwise df index is only first level of
             # exploded GeoSeries index.
-            df.set_index(exploded_index, inplace=True)
-            df.index.names = list(self.index.names) + [None]
-        else:
-            df.set_index(exploded_index, inplace=True)
-            df.index.names = self.index.names
-
-        if f"__{level_str}" in df.columns:
-            df = df.rename(columns={f"__{level_str}": level_str})
+            df = df.set_index(
+                exploded_geom.index.droplevel(
+                    list(range(exploded_geom.index.nlevels - 1))
+                ),
+                append=True,
+            )
 
         return df
 


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -1090,6 +1090,109 @@ def test_explode_pandas_multi_index_ignore_index(self, outer_index):
         test_df = df.explode(ignore_index=True, index_parts=True)
         assert_frame_equal(test_df, expected_df)
 
+    def test_explode_order(self):
+        df = GeoDataFrame(
+            {"vals": [1, 2, 3]},
+            geometry=[MultiPoint([(x, x), (x, 0)]) for x in range(3)],
+            index=[2, 9, 7],
+        )
+        test_df = df.explode(index_parts=True)
+
+        expected_index = MultiIndex.from_arrays(
+            [[2, 2, 9, 9, 7, 7], [0, 1, 0, 1, 0, 1]],
+        )
+        expected_geometry = GeoSeries(
+            [
+                Point(0, 0),
+                Point(0, 0),
+                Point(1, 1),
+                Point(1, 0),
+                Point(2, 2),
+                Point(2, 0),
+            ],
+            index=expected_index,
+        )
+        expected_df = GeoDataFrame(
+            {"vals": [1, 1, 2, 2, 3, 3]},
+            geometry=expected_geometry,
+            index=expected_index,
+        )
+        assert_geodataframe_equal(test_df, expected_df)
+
+    def test_explode_order_no_multi(self):
+        df = GeoDataFrame(
+            {"vals": [1, 2, 3]},
+            geometry=[Point(0, x) for x in range(3)],
+            index=[2, 9, 7],
+        )
+        test_df = df.explode(index_parts=True)
+
+        expected_index = MultiIndex.from_arrays(
+            [[2, 9, 7], [0, 0, 0]],
+        )
+        expected_df = GeoDataFrame(
+            {"vals": [1, 2, 3]},
+            geometry=[Point(0, x) for x in range(3)],
+            index=expected_index,
+        )
+        assert_geodataframe_equal(test_df, expected_df)
+
+    def test_explode_order_mixed(self):
+        df = GeoDataFrame(
+            {"vals": [1, 2, 3]},
+            geometry=[MultiPoint([(x, x), (x, 0)]) for x in range(2)] + [Point(0, 10)],
+            index=[2, 9, 7],
+        )
+        test_df = df.explode(index_parts=True)
+
+        expected_index = MultiIndex.from_arrays(
+            [[2, 2, 9, 9, 7], [0, 1, 0, 1, 0]],
+        )
+        expected_geometry = GeoSeries(
+            [
+                Point(0, 0),
+                Point(0, 0),
+                Point(1, 1),
+                Point(1, 0),
+                Point(0, 10),
+            ],
+            index=expected_index,
+        )
+        expected_df = GeoDataFrame(
+            {"vals": [1, 1, 2, 2, 3]},
+            geometry=expected_geometry,
+            index=expected_index,
+        )
+        assert_geodataframe_equal(test_df, expected_df)
+
+    def test_explode_duplicated_index(self):
+        df = GeoDataFrame(
+            {"vals": [1, 2, 3]},
+            geometry=[MultiPoint([(x, x), (x, 0)]) for x in range(3)],
+            index=[1, 1, 2],
+        )
+        test_df = df.explode(index_parts=True)
+        expected_index = MultiIndex.from_arrays(
+            [[1, 1, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]],
+        )
+        expected_geometry = GeoSeries(
+            [
+                Point(0, 0),
+                Point(0, 0),
+                Point(1, 1),
+                Point(1, 0),
+                Point(2, 2),
+                Point(2, 0),
+            ],
+            index=expected_index,
+        )
+        expected_df = GeoDataFrame(
+            {"vals": [1, 1, 2, 2, 3, 3]},
+            geometry=expected_geometry,
+            index=expected_index,
+        )
+        assert_geodataframe_equal(test_df, expected_df)
+
     #
     # Test '&', '|', '^', and '-'
     #