Repository: geopandas/geopandas

Issue:
REGR: pickling shapely-backed gdf raises TypeError
If you try to pickle geodataframe without pygeos, it raises a `TypeError`. I am not sure why since the condition in `array.py` should not use the line which errors.

```python
import geopandas

geopandas.options.use_pygeos = False
df = geopandas.read_file(geopandas.datasets.get_path('nybb'))
df.to_pickle('tst.pkl')
```
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-a48b53e8f4f1> in <module>
----> 1 df.to_pickle('tst.pkl')

/opt/miniconda3/envs/geo_dev/lib/python3.7/site-packages/pandas/core/generic.py in to_pickle(self, path, compression, protocol)
   2674         from pandas.io.pickle import to_pickle
   2675 
-> 2676         to_pickle(self, path, compression=compression, protocol=protocol)
   2677 
   2678     def to_clipboard(

/opt/miniconda3/envs/geo_dev/lib/python3.7/site-packages/pandas/io/pickle.py in to_pickle(obj, filepath_or_buffer, compression, protocol)
     85         protocol = pickle.HIGHEST_PROTOCOL
     86     try:
---> 87         f.write(pickle.dumps(obj, protocol=protocol))
     88     finally:
     89         f.close()

~/Git/geopandas/martinfleis/geopandas/array.py in __getstate__(self)
    421 
    422         def __getstate__(self):
--> 423             return (pygeos.to_wkb(self.data), self._crs)
    424 
    425         def __setstate__(self, state):

/opt/miniconda3/envs/geo_dev/lib/python3.7/site-packages/pygeos/io.py in to_wkb(geometry, hex, output_dimension, byte_order, include_srid, **kwargs)
    156         np.intc(byte_order),
    157         np.bool(include_srid),
--> 158         **kwargs,
    159     )
    160 

TypeError: One of the arguments is of incorrect type. Please provide only Geometry objects.
```

Discussion:

Patch:
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index a1499d30..a657d21f 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -96,13 +96,14 @@ Affine transformations
    GeoSeries.skew
    GeoSeries.translate
 
-Aggregating methods
--------------------
+Aggregating and exploding
+-------------------------
 
 .. autosummary::
    :toctree: api/
 
    GeoSeries.unary_union
+   GeoSeries.explode
 
 Reading and writing files
 -------------------------
diff --git a/geopandas/array.py b/geopandas/array.py
index e9a3ae0b..9c3f7fff 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -417,20 +417,20 @@ class GeometryArray(ExtensionArray):
         #             "and CRS of existing geometries."
         #         )
 
-    if compat.USE_PYGEOS:
-
-        def __getstate__(self):
+    def __getstate__(self):
+        if compat.USE_PYGEOS:
             return (pygeos.to_wkb(self.data), self._crs)
+        else:
+            return self.__dict__
 
-        def __setstate__(self, state):
+    def __setstate__(self, state):
+        if compat.USE_PYGEOS:
             geoms = pygeos.from_wkb(state[0])
             self._crs = state[1]
+            self._sindex = None  # pygeos.STRtree could not be pickled yet
             self.data = geoms
             self.base = None
-
-    else:
-
-        def __setstate__(self, state):
+        else:
             if "_crs" not in state:
                 state["_crs"] = None
             self.__dict__.update(state)
diff --git a/geopandas/base.py b/geopandas/base.py
index e7cfed28..e675a515 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -2,14 +2,12 @@ from warnings import warn
 
 import numpy as np
 import pandas as pd
-from pandas import DataFrame, MultiIndex, Series
+from pandas import DataFrame, Series
 
 from shapely.geometry import box
 from shapely.geometry.base import BaseGeometry
 from shapely.ops import cascaded_union
 
-import geopandas as gpd
-
 from .array import GeometryArray, GeometryDtype
 
 
@@ -1200,54 +1198,6 @@ GeometryCollection
             "skew", self, xs, ys, origin=origin, use_radians=use_radians
         )
 
-    def explode(self):
-        """
-        Explode multi-part geometries into multiple single geometries.
-
-        Single rows can become multiple rows.
-        This is analogous to PostGIS's ST_Dump(). The 'path' index is the
-        second level of the returned MultiIndex
-
-        Returns
-        ------
-        A GeoSeries with a MultiIndex. The levels of the MultiIndex are the
-        original index and a zero-based integer index that counts the
-        number of single geometries within a multi-part geometry.
-
-        Examples
-        --------
-        >>> from shapely.geometry import MultiPoint
-        >>> s = geopandas.GeoSeries(
-        ...     [MultiPoint([(0, 0), (1, 1)]), MultiPoint([(2, 2), (3, 3), (4, 4)])]
-        ... )
-        >>> s
-        0        MULTIPOINT (0.00000 0.00000, 1.00000 1.00000)
-        1    MULTIPOINT (2.00000 2.00000, 3.00000 3.00000, ...
-        dtype: geometry
-
-        >>> s.explode()
-        0  0    POINT (0.00000 0.00000)
-           1    POINT (1.00000 1.00000)
-        1  0    POINT (2.00000 2.00000)
-           1    POINT (3.00000 3.00000)
-           2    POINT (4.00000 4.00000)
-        dtype: geometry
-
-        """
-        index = []
-        geometries = []
-        for idx, s in self.geometry.iteritems():
-            if s.type.startswith("Multi") or s.type == "GeometryCollection":
-                geoms = s.geoms
-                idxs = [(idx, i) for i in range(len(geoms))]
-            else:
-                geoms = [s]
-                idxs = [(idx, 0)]
-            index.extend(idxs)
-            geometries.extend(geoms)
-        index = MultiIndex.from_tuples(index, names=self.index.names + [None])
-        return gpd.GeoSeries(geometries, index=index, crs=self.crs).__finalize__(self)
-
     @property
     def cx(self):
         """
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index e9a2047b..d122e531 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1288,8 +1288,8 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
 
         return aggregated
 
-    # overrides GeoPandasBase method
-    def explode(self):
+    # overrides the pandas native explode method to break up features geometrically
+    def explode(self, column=None, **kwargs):
         """
         Explode muti-part geometries into multiple single geometries.
 
@@ -1333,6 +1333,15 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
         1 0  name2  POINT (2.00000 1.00000)
           1  name2  POINT (0.00000 0.00000)
         """
+
+        # If no column is specified then default to the active geometry column
+        if column is None:
+            column = self.geometry.name
+        # If the specified column is not a geometry dtype use pandas explode
+        if not isinstance(self[column].dtype, GeometryDtype):
+            return super(GeoDataFrame, self).explode(column, **kwargs)
+            # TODO: make sure index behaviour is consistent
+
         df_copy = self.copy()
 
         if "level_1" in df_copy.columns:  # GH1393
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 02e5f11d..e6718085 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -3,7 +3,7 @@ import warnings
 
 import numpy as np
 import pandas as pd
-from pandas import Series
+from pandas import Series, MultiIndex
 from pandas.core.internals import SingleBlockManager
 
 from pyproj import CRS, Transformer
@@ -590,6 +590,58 @@ class GeoSeries(GeoPandasBase, Series):
 
     plot.__doc__ = plot_series.__doc__
 
+    def explode(self):
+        """
+        Explode multi-part geometries into multiple single geometries.
+
+        Single rows can become multiple rows.
+        This is analogous to PostGIS's ST_Dump(). The 'path' index is the
+        second level of the returned MultiIndex
+
+        Returns
+        ------
+        A GeoSeries with a MultiIndex. The levels of the MultiIndex are the
+        original index and a zero-based integer index that counts the
+        number of single geometries within a multi-part geometry.
+
+        Examples
+        --------
+        >>> from shapely.geometry import MultiPoint
+        >>> s = geopandas.GeoSeries(
+        ...     [MultiPoint([(0, 0), (1, 1)]), MultiPoint([(2, 2), (3, 3), (4, 4)])]
+        ... )
+        >>> s
+        0        MULTIPOINT (0.00000 0.00000, 1.00000 1.00000)
+        1    MULTIPOINT (2.00000 2.00000, 3.00000 3.00000, ...
+        dtype: geometry
+
+        >>> s.explode()
+        0  0    POINT (0.00000 0.00000)
+           1    POINT (1.00000 1.00000)
+        1  0    POINT (2.00000 2.00000)
+           1    POINT (3.00000 3.00000)
+           2    POINT (4.00000 4.00000)
+        dtype: geometry
+
+        See also
+        --------
+        GeoDataFrame.explode
+
+        """
+        index = []
+        geometries = []
+        for idx, s in self.geometry.iteritems():
+            if s.type.startswith("Multi") or s.type == "GeometryCollection":
+                geoms = s.geoms
+                idxs = [(idx, i) for i in range(len(geoms))]
+            else:
+                geoms = [s]
+                idxs = [(idx, 0)]
+            index.extend(idxs)
+            geometries.extend(geoms)
+        index = MultiIndex.from_tuples(index, names=self.index.names + [None])
+        return GeoSeries(geometries, index=index, crs=self.crs).__finalize__(self)
+
     #
     # Additional methods
     #


Test patch:
diff --git a/geopandas/io/tests/test_pickle.py b/geopandas/io/tests/test_pickle.py
index 081d0ad2..0327714d 100644
--- a/geopandas/io/tests/test_pickle.py
+++ b/geopandas/io/tests/test_pickle.py
@@ -14,7 +14,8 @@ import pyproj
 import pytest
 from geopandas.testing import assert_geodataframe_equal
 from geopandas import _compat as compat
-
+import geopandas
+from shapely.geometry import Point
 
 DATA_PATH = pathlib.Path(os.path.dirname(__file__)) / "data"
 
@@ -35,6 +36,14 @@ def legacy_pickle(request):
     return request.param
 
 
+@pytest.fixture
+def with_use_pygeos_false():
+    orig = geopandas.options.use_pygeos
+    geopandas.options.use_pygeos = not orig
+    yield
+    geopandas.options.use_pygeos = orig
+
+
 @pytest.mark.skipif(
     compat.USE_PYGEOS or (str(pyproj.__version__) < LooseVersion("2.4")),
     reason=(
@@ -58,3 +67,16 @@ def test_round_trip_current(tmpdir, current_pickle_data):
         value.to_pickle(path)
         result = pd.read_pickle(path)
         assert_geodataframe_equal(result, value)
+        assert isinstance(result.has_sindex, bool)
+
+
+@pytest.mark.skipif(not compat.HAS_PYGEOS, reason="requires pygeos to test #1745")
+def test_pygeos_switch(tmpdir, with_use_pygeos_false):
+    gdf_crs = geopandas.GeoDataFrame(
+        {"a": [0.1, 0.2, 0.3], "geometry": [Point(1, 1), Point(2, 2), Point(3, 3)]},
+        crs="EPSG:4326",
+    )
+    path = str(tmpdir / "gdf_crs.pickle")
+    gdf_crs.to_pickle(path)
+    result = pd.read_pickle(path)
+    assert_geodataframe_equal(result, gdf_crs)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 9a0638a5..92e6e4de 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -11,6 +11,7 @@ from shapely.ops import unary_union
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.base import GeoPandasBase
 
+from geopandas.testing import assert_geodataframe_equal
 from geopandas.tests.util import assert_geoseries_equal, geom_almost_equals, geom_equals
 from geopandas import _compat as compat
 from pandas.testing import assert_frame_equal, assert_series_equal
@@ -741,6 +742,75 @@ class TestGeomMethods:
         expected_df = expected_df.set_index(expected_index)
         assert_frame_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        not compat.PANDAS_GE_025,
+        reason="pandas explode introduced in pandas 0.25",
+    )
+    def test_explode_pandas_fallback(self):
+        d = {
+            "col1": [["name1", "name2"], ["name3", "name4"]],
+            "geometry": [
+                MultiPoint([(1, 2), (3, 4)]),
+                MultiPoint([(2, 1), (0, 0)]),
+            ],
+        }
+        gdf = GeoDataFrame(d, crs=4326)
+        expected_df = GeoDataFrame(
+            {
+                "col1": ["name1", "name2", "name3", "name4"],
+                "geometry": [
+                    MultiPoint([(1, 2), (3, 4)]),
+                    MultiPoint([(1, 2), (3, 4)]),
+                    MultiPoint([(2, 1), (0, 0)]),
+                    MultiPoint([(2, 1), (0, 0)]),
+                ],
+            },
+            index=[0, 0, 1, 1],
+            crs=4326,
+        )
+
+        # Test with column provided as arg
+        exploded_df = gdf.explode("col1")
+        assert_geodataframe_equal(exploded_df, expected_df)
+
+        # Test with column provided as kwarg
+        exploded_df = gdf.explode(column="col1")
+        assert_geodataframe_equal(exploded_df, expected_df)
+
+    @pytest.mark.skipif(
+        not compat.PANDAS_GE_11,
+        reason="ignore_index keyword introduced in pandas 1.1.0",
+    )
+    def test_explode_pandas_fallback_ignore_index(self):
+        d = {
+            "col1": [["name1", "name2"], ["name3", "name4"]],
+            "geometry": [
+                MultiPoint([(1, 2), (3, 4)]),
+                MultiPoint([(2, 1), (0, 0)]),
+            ],
+        }
+        gdf = GeoDataFrame(d, crs=4326)
+        expected_df = GeoDataFrame(
+            {
+                "col1": ["name1", "name2", "name3", "name4"],
+                "geometry": [
+                    MultiPoint([(1, 2), (3, 4)]),
+                    MultiPoint([(1, 2), (3, 4)]),
+                    MultiPoint([(2, 1), (0, 0)]),
+                    MultiPoint([(2, 1), (0, 0)]),
+                ],
+            },
+            crs=4326,
+        )
+
+        # Test with column provided as arg
+        exploded_df = gdf.explode("col1", ignore_index=True)
+        assert_geodataframe_equal(exploded_df, expected_df)
+
+        # Test with column provided as kwarg
+        exploded_df = gdf.explode(column="col1", ignore_index=True)
+        assert_geodataframe_equal(exploded_df, expected_df)
+
     #
     # Test '&', '|', '^', and '-'
     #