Repository: geopandas/geopandas (python)

Issue:
BUG: Missing _crs attribute when using sjoin on grouped gdf and latest version of pandas
- [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 main branch of geopandas.

---

#### Code Sample, a copy-pastable example

```python
import geopandas as gpd
from geopandas import sjoin
from shapely.geometry import MultiPoint


pts = MultiPoint([[0, 0], [1, 1]])
polys = [_.buffer(0.1) for _ in pts.geoms]
gdf = gpd.GeoDataFrame(["a", "b"], geometry=polys)

df_close = gdf.groupby(0).apply(lambda x: sjoin(x, x, how="inner",
                                                predicate="overlaps"))
```

#### Problem description

Unsure if this is a pandas or a geopandas problem. With pandas 1.2.5, the MWE above works fine. With pandas 1.3.5, it fails; here's the traceback:

```python
Traceback (most recent call last):
  File "/Users/kylepenner/Desktop/bug.py", line 10, in <module>
    df_close = gdf.groupby(0).apply(lambda x: sjoin(x, x, how="inner",
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/pandas/core/groupby/groupby.py", line 1275, in apply
    result = self._python_apply_general(f, self._selected_obj)
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/pandas/core/groupby/groupby.py", line 1309, in _python_apply_general
    keys, values, mutated = self.grouper.apply(f, data, self.axis)
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/pandas/core/groupby/ops.py", line 852, in apply
    res = f(group)
  File "/Users/kylepenner/Desktop/bug.py", line 10, in <lambda>
    df_close = gdf.groupby(0).apply(lambda x: sjoin(x, x, how="inner",
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/geopandas/tools/sjoin.py", line 122, in sjoin
    _basic_checks(left_df, right_df, how, lsuffix, rsuffix)
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/geopandas/tools/sjoin.py", line 165, in _basic_checks
    if not _check_crs(left_df, right_df):
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/geopandas/array.py", line 68, in _check_crs
    if not left.crs == right.crs:
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/pandas/core/generic.py", line 5487, in __getattr__
    return object.__getattribute__(self, name)
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/geopandas/geodataframe.py", line 408, in crs
    return self._crs
  File "/Users/kylepenner/miniconda3/envs/test2/lib/python3.10/site-packages/pandas/core/generic.py", line 5487, in __getattr__
    return object.__getattribute__(self, name)
AttributeError: 'GeoDataFrame' object has no attribute '_crs'
```

#### Expected Output

A GeoDataFrame.

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.10.1 | packaged by conda-forge | (main, Dec 22 2021, 01:39:14) [Clang 11.1.0 ]
executable : /Users/kylepenner/miniconda3/envs/test2/bin/python
machine    : macOS-11.6.2-x86_64-i386-64bit

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.10.1
GEOS lib   : /Users/kylepenner/miniconda3/envs/test2/lib/libgeos_c.dylib
GDAL       : 3.4.1
GDAL data dir: /Users/kylepenner/miniconda3/envs/test2/share/gdal
PROJ       : 8.2.1
PROJ data dir: /Users/kylepenner/miniconda3/envs/test2/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.10.2
pandas     : 1.3.5
fiona      : 1.8.20
numpy      : 1.22.0
shapely    : 1.8.0
rtree      : 0.9.7
pyproj     : 3.3.0
matplotlib : 3.5.1
mapclassify: 2.4.3
geopy      : None
psycopg2   : None
geoalchemy2: None
pyarrow    : None
pygeos     : None

</details>


Pull request:
TST: test groupby apply with function that requires GeoDataFrame attributes
Closes #2294

https://github.com/geopandas/geopandas/issues/2294 needs to be fixed on the pandas side (https://github.com/pandas-dev/pandas/pull/45363), but adding a test here as well in GeoPandas with the original reproducer (in pandas it's only a test with a dummy subclass)

Patch:
diff --git a/doc/source/docs/user_guide/aggregation_with_dissolve.rst b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
index f6952dd3..c8a10cd3 100644
--- a/doc/source/docs/user_guide/aggregation_with_dissolve.rst
+++ b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
@@ -75,3 +75,25 @@ However it also accepts other summary statistic options as allowed by :meth:`pan
 * 'sum'
 * 'mean'
 * 'median'
+* function
+* string function name
+* list of functions and/or function names, e.g. [np.sum, 'mean']
+* dict of axis labels -> functions, function names or list of such.
+
+For example, to get the number of contries on each continent, 
+as well as the populations of the largest and smallest country of each,
+we can aggregate the ``'name'`` column using ``'count'``,
+and the ``'pop_est'`` column using ``'min'`` and ``'max'``:
+
+.. ipython:: python
+
+    world = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+    continents = world.dissolve(
+        by="continent",
+        aggfunc={
+            "name": "count",
+            "pop_est": ["min", "max"],
+        },
+    )
+
+   continents.head()
\ No newline at end of file
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index 000db2a6..f233eee8 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -19,6 +19,7 @@ PANDAS_GE_10 = str(pd.__version__) >= LooseVersion("1.0.0")
 PANDAS_GE_11 = str(pd.__version__) >= LooseVersion("1.1.0")
 PANDAS_GE_115 = str(pd.__version__) >= LooseVersion("1.1.5")
 PANDAS_GE_12 = str(pd.__version__) >= LooseVersion("1.2.0")
+PANDAS_GE_13 = str(pd.__version__) >= LooseVersion("1.3.0")
 PANDAS_GE_14 = str(pd.__version__) >= LooseVersion("1.4.0")
 
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index e29b2213..25c27933 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1476,6 +1476,12 @@ individually so that features may have different properties
         aggfunc : function or string, default "first"
             Aggregation function for manipulation of data associated
             with each group. Passed to pandas `groupby.agg` method.
+            Accepted combinations are:
+
+            - function
+            - string function name
+            - list of functions and/or function names, e.g. [np.sum, 'mean']
+            - dict of axis labels -> functions, function names or list of such.
         as_index : boolean, default True
             If true, groupby columns become index of result.
         level : int or str or sequence of int or sequence of str, default None
diff --git a/readthedocs.yml b/readthedocs.yml
index 6e710093..af979a2f 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,9 +1,12 @@
 version: 2
-formats: []
-conda:
-    environment: doc/environment.yml
+build:
+  os: ubuntu-20.04
+  tools:
+    python: mambaforge-4.10
 python:
-    version: 3
-    install:
-        - method: pip
-          path: .
+  install:
+  - method: pip
+    path: .
+conda:
+  environment: doc/environment.yml
+formats: []


Test patch:
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index ed080af4..c6d04209 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -558,6 +558,44 @@ def test_groupby_groups(df):
     assert_frame_equal(res, exp)
 
 
+@pytest.mark.skipif(
+    compat.PANDAS_GE_13 and not compat.PANDAS_GE_14,
+    reason="this was broken in pandas 1.3.5 (GH-2294)",
+)
+@pytest.mark.parametrize("crs", [None, "EPSG:4326"])
+def test_groupby_metadata(crs):
+    # https://github.com/geopandas/geopandas/issues/2294
+    df = GeoDataFrame(
+        {
+            "geometry": [Point(0, 0), Point(1, 1), Point(0, 0)],
+            "value1": np.arange(3, dtype="int64"),
+            "value2": np.array([1, 2, 1], dtype="int64"),
+        },
+        crs=crs,
+    )
+
+    # dummy test asserting we can access the crs
+    def func(group):
+        assert isinstance(group, GeoDataFrame)
+        assert group.crs == crs
+
+    df.groupby("value2").apply(func)
+
+    # actual test with functionality
+    res = df.groupby("value2").apply(
+        lambda x: geopandas.sjoin(x, x[["geometry", "value1"]], how="inner")
+    )
+
+    expected = (
+        df.take([0, 2, 0, 2, 1])
+        .set_index("value2", drop=False, append=True)
+        .swaplevel()
+        .rename(columns={"value1": "value1_left"})
+        .assign(value1_right=[0, 0, 2, 2, 1])
+    )
+    assert_geodataframe_equal(res.drop(columns=["index_right"]), expected)
+
+
 def test_apply(s):
     # function that returns geometry preserves GeoSeries class
     def geom_func(geom):