Repository: geopandas/geopandas

Issue:
GeoSeries.equals TypeError
The `GeoSeries.equals()` method raises a TypeError.

```python
from geopandas import GeoSeries
from shapely.geometry import Point

a = GeoSeries([Point(1, 1)])
b = GeoSeries([Point(1, 1)])
a.equals(b)
```
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in 
      4 a = GeoSeries([Point(1, 1)])
      5 b = GeoSeries([Point(1, 1)])
----> 6 a.equals(b)

~/Git/dba.positions/venv/lib/python3.7/site-packages/pandas/core/generic.py in equals(self, other)
   1431         False
   1432         """
-> 1433         if not isinstance(other, self._constructor):
   1434             return False
   1435         return self._data.equals(other._data)

TypeError: isinstance() arg 2 must be a type or tuple of types
```




Environment: 
```
3.7.7 (default, Mar 10 2020, 15:16:38)
[GCC 7.5.0]

attrs==19.3.0
click==7.1.2
click-plugins==1.1.1
cligj==0.5.0
Fiona==1.8.13.post1
geopandas==0.7.0
munch==2.5.0
numpy==1.18.4
pandas==1.0.3
pkg-resources==0.0.0
pyproj==2.6.1.post1
python-dateutil==2.8.1
pytz==2020.1
Shapely==1.7.0
six==1.14.0
```



Discussion:
Interesting. This looks like an issue with the fact that [the series constructor is not a class](https://github.com/geopandas/geopandas/blob/f13dafb881eaaf2bbf296e6f12189cc2c21a4216/geopandas/geoseries.py#L26)

We might be able to get around this by making this a class `MaybeGeoSeries` where this is part of its `__new__` method? 

```python
class MaybeGeoSeries(GeoSeries):
    def __new__(cls, *args, **kwargs):
        try:
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    "ignore",
                    message=_SERIES_WARNING_MSG,
                    category=FutureWarning,
                    module="geopandas[.*]",
                )
                return GeoSeries(data=data, index=index, crs=crs, **kwargs)
        except TypeError:
            return Series(data=data, index=index, **kwargs)

```
@simon-keith Thanks for the report! That's an issue in pandas that they shoudn't check against `_constructor`. Will report it over there.
Ah, yes, making that a class could also be possible. But I would rather see this fixed in pandas so they don't rely on `_constructor` being a class.  
Also, the above won't work I think, as this `MaybeGeoSeries` is a subclass of GeoSeries, so an actual GeoSeries will not be an instance of the `MaybeGeoSeries`. So to get this to work like that, we should add an isinstance hook to that class (which is certainly possible).

An alternative to fix it right now in GeoPandas could also be to override `Series.equals` in GeoSeries to avoid that check (until it is fixed in pandas).
> we should add an isinstance hook to that class (which is certainly possible).

That would be with ``__instancecheck__``
Sure, I think the proper solution is to get pandas changed to allow for non-class constructors, if that's not a requirement elsewhere! 
From a quick search, it seems the only case where it is not used to actually construct an object for DataFrame/Series (except for some tests)

Resolving patch:
diff --git a/geopandas/base.py b/geopandas/base.py
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -790,6 +790,34 @@ def cx(self):
         """
         return _CoordinateIndexer(self)
 
+    def equals(self, other):
+        """
+        Test whether two objects contain the same elements.
+
+        This function allows two GeoSeries or GeoDataFrames to be compared
+        against each other to see if they have the same shape and elements.
+        Missing values in the same location are considered equal. The
+        row/column index do not need to have the same type (as long as the
+        values are still considered equal), but the dtypes of the respective
+        columns must be the same.
+
+        Parameters
+        ----------
+        other : GeoSeries or GeoDataFrame
+            The other GeoSeries or GeoDataFrame to be compared with the first.
+
+        Returns
+        -------
+        bool
+            True if all elements are the same in both objects, False
+            otherwise.
+        """
+        # we override this because pandas is using `self._constructor` in the
+        # isinstance check (https://github.com/geopandas/geopandas/issues/1420)
+        if not isinstance(other, type(self)):
+            return False
+        return self._data.equals(other._data)
+
 
 class _CoordinateIndexer(object):
     # see docstring GeoPandasBase.cx property above


Test patch:
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -314,6 +314,22 @@ def test_select_dtypes(df):
     assert_frame_equal(res, exp)
 
 
+def test_equals(s, df):
+    # https://github.com/geopandas/geopandas/issues/1420
+    s2 = s.copy()
+    assert s.equals(s2) is True
+    s2.iloc[0] = None
+    assert s.equals(s2) is False
+
+    df2 = df.copy()
+    assert df.equals(df2) is True
+    df2.loc[0, "geometry"] = Point(10, 10)
+    assert df.equals(df2) is False
+    df2 = df.copy()
+    df2.loc[0, "value1"] = 10
+    assert df.equals(df2) is False
+
+
 # Missing values
 
 
@@ -460,7 +476,7 @@ def test_groupby(df):
 
     # apply on geometry column not resulting in new geometry
     res = df.groupby("value2")["geometry"].apply(lambda x: x.unary_union.area)
-    exp = pd.Series([0.0, 0.0], index=pd.Index([1, 2], name="value2"), name="geometry",)
+    exp = pd.Series([0.0, 0.0], index=pd.Index([1, 2], name="value2"), name="geometry")
 
     assert_series_equal(res, exp)