Repository: geopandas/geopandas

Issue:
BUG: 'GeometryArray' object has no attribute 'value_counts'
- [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.

---

#### Code Sample, a copy-pastable example

```python
gdf["geometry"].value_counts()
```
<details>

```raw
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-fb6329993e01> in <module>
----> 1 gdf["geometry"].value_counts()

C:\tools\miniconda3\envs\project-dev\lib\site-packages\pandas\core\base.py in value_counts(self, normalize, sort, ascending, bins, dropna)
   1243         dtype: int64
   1244         """
-> 1245         result = value_counts(
   1246             self,
   1247             sort=sort,

C:\tools\miniconda3\envs\project-dev\lib\site-packages\pandas\core\algorithms.py in value_counts(values, sort, ascending, normalize, bins, dropna)
    790 
    791             # handle Categorical and sparse,
--> 792             result = Series(values)._values.value_counts(dropna=dropna)
    793             result.name = name
    794             counts = result._values

AttributeError: 'GeometryArray' object has no attribute 'value_counts'
```

</details>

#### Problem description

The "geometry" columns of a geopandas data frame should act like a regular pandas Series. This includes supporting "value_counts". 

I can't even monkey-patch it with something that uses `collections.Counter` since the geometry types are unhashable, i.e. the following also fails:

```
import collections
pd.Series(collections.Counter(gdf["geometry"]), name="geometry")
```

#### Expected Output


Same as `pd.Series.value_counts(*args, **kwargs)`

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.8.6 | packaged by conda-forge | (default, Nov 27 2020, 18:58:29) [MSC v.1916 64 bit (AMD64)]
executable : C:\tools\miniconda3\envs\project-dev\python.exe
machine    : Windows-10-10.0.18362-SP0

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : None
GEOS lib   : None
GDAL       : 3.1.4
GDAL data dir: None
PROJ       : 7.1.1
PROJ data dir: C:\tools\miniconda3\envs\project-dev\Library\share\proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.8.1
pandas     : 1.1.5
fiona      : 1.8.18
numpy      : 1.19.4
shapely    : 1.7.1
rtree      : 0.9.4
pyproj     : 2.6.1.post1
matplotlib : 3.3.3
mapclassify: 2.3.0
geopy      : 2.0.0
psycopg2   : None
geoalchemy2: None
pyarrow    : 2.0.0

</details>



Discussion:
@jamesmyatt Thanks for the report! Yes, `value_counts` is currently not implemented. In the past several of those functionalities were not working (eg like groupby, giving the "not hashable" error). But nowadays we actually use the WKB representation for those, so we should also do that for `value_counts`.
Thanks. It doesn't need to be clever. Just needs to work 😁 .

It would also be OK for me if it depended on the PyGEOS backend.

It will be a while before the collections.Counter workaround can work with Shapely: https://github.com/Toblerity/Shapely/pull/985
Would it be simple to do something like convert the geometry series to into a regular pandas binary series using WKB, then use regular value_counts and then convert the index back to a geometry type?

Alternatively, would it be OK to convert it to a text series using WKT, then use regular value_counts and just return that directly?

These are just ideas.
> Would it be simple to do something like convert the geometry series to into a regular pandas binary series using WKB, then use regular value_counts and then convert the index back to a geometry type?

Yes, that would be the idea. Something like this (untested code), method on GeometryArray:

```
def value_counts(self, ...):
    values = to_wkb(self)
    from pandas import Series, Index
    result = Series(values).value_counts(...)
    result.index = Index(from_wkb(result.index))
```

I think I'll work on this as well, seems pretty straightforward and nice to have.
@m-richards Thanks!

Resolving patch:
diff --git a/geopandas/array.py b/geopandas/array.py
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1044,6 +1044,38 @@ def isna(self):
         else:
             return np.array([g is None for g in self.data], dtype="bool")
 
+    def value_counts(
+        self,
+        dropna: bool = True,
+    ):
+        """
+        Compute a histogram of the counts of non-null values.
+
+        Parameters
+        ----------
+        dropna : bool, default True
+            Don't include counts of NaN
+
+        Returns
+        -------
+        pd.Series
+        """
+
+        # note ExtensionArray usage of value_counts only specifies dropna,
+        # so sort, normalize and bins are not arguments
+        values = to_wkb(self)
+        from pandas import Series, Index
+
+        result = Series(values).value_counts(dropna=dropna)
+        # value_counts converts None to nan, need to convert back for from_wkb to work
+        # note result.index already has object dtype, not geometry
+        # Can't use fillna(None) or Index.putmask, as this gets converted back to nan
+        # for object dtypes
+        result.index = Index(
+            from_wkb(np.where(result.index.isna(), None, result.index))
+        )
+        return result
+
     def unique(self):
         """Compute the ExtensionArray of unique values.
 


Test patch:
diff --git a/geopandas/tests/test_extension_array.py b/geopandas/tests/test_extension_array.py
--- a/geopandas/tests/test_extension_array.py
+++ b/geopandas/tests/test_extension_array.py
@@ -434,12 +434,12 @@ def test_compare_array(self, data, all_compare_operators):  # noqa
 
 
 class TestMethods(extension_tests.BaseMethodsTests):
-    @not_yet_implemented
+    @no_sorting
     @pytest.mark.parametrize("dropna", [True, False])
     def test_value_counts(self, all_data, dropna):
         pass
 
-    @not_yet_implemented
+    @no_sorting
     def test_value_counts_with_normalize(self, data):
         pass
 
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
@@ -5,7 +5,7 @@
 import pandas as pd
 
 import shapely
-from shapely.geometry import Point, GeometryCollection
+from shapely.geometry import Point, GeometryCollection, LineString
 
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries
@@ -408,13 +408,31 @@ def test_unique():
     assert_array_equal(s.unique(), exp)
 
 
-@pytest.mark.xfail
 def test_value_counts():
     # each object is considered unique
     s = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)])
     res = s.value_counts()
     exp = pd.Series([2, 1], index=[Point(0, 0), Point(1, 1)])
     assert_series_equal(res, exp)
+    # Check crs doesn't make a difference - note it is not kept in output index anyway
+    s2 = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)], crs="EPSG:4326")
+    res2 = s2.value_counts()
+    assert_series_equal(res2, exp)
+
+    # check mixed geometry
+    s3 = GeoSeries([Point(0, 0), LineString([[1, 1], [2, 2]]), Point(0, 0)])
+    res3 = s3.value_counts()
+    exp3 = pd.Series([2, 1], index=[Point(0, 0), LineString([[1, 1], [2, 2]])])
+    assert_series_equal(res3, exp3)
+
+    # check None is handled
+    s4 = GeoSeries([Point(0, 0), None, Point(0, 0)])
+    res4 = s4.value_counts(dropna=True)
+    exp4_dropna = pd.Series([2], index=[Point(0, 0)])
+    assert_series_equal(res4, exp4_dropna)
+    exp4_keepna = pd.Series([2, 1], index=[Point(0, 0), None])
+    res4_keepna = s4.value_counts(dropna=False)
+    assert_series_equal(res4_keepna, exp4_keepna)
 
 
 @pytest.mark.xfail(strict=False)