Repository: geopandas/geopandas

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>



Discussion:
@kdpenner thanks for the report!

I _suppose_ this will probably a regression in pandas 1.2->1.3 rather than in geopandas. Potentially related to https://github.com/pandas-dev/pandas/pull/40236

Resolving patch:
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -19,6 +19,7 @@
 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")
 
 


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
@@ -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):