Repository: geopandas/geopandas

Issue:
BUG/DISCUSSION: GeometryTypeError: Unknown geometry type: 'featurecollection'
When applying some (nested) functions to a geodataframe, using the `gdf.apply(lambda r: f(r, arg1, ...), axis=1)` approach, that return different type geometries than the ones in the source dataframe, you get errors that are often quite cryptic.  

Please find an example below. Maybe such workflows are not ideal, but I often find myself coming back to these kind of implementations.. This snippet raises`GeometryTypeError: Unknown geometry type: 'featurecollection'`, which might be triggered because the result geodataframe has different geometries types than the one where the outer function was applied to. When the data is returned as Pandas dataframe it works as expected. 

#### Code Sample, a copy-pastable example

```python
import geopandas as gpd
import pandas as pd
import shapely.geometry


def f1(row: pd.Series, arg1):
    # Sample data for point geoms that would be calculated in this function
    point_coords = [(0, 0), (0, 1), (1, 1), (1, 0)]
    geom = [shapely.geometry.Point(coords) for coords in point_coords]
    gdf = gpd.GeoDataFrame(geometry=geom, crs=row["src_crs"])
    # commented lines below work as expected.
    # df  = pd.DataFrame({"geometry":geom})  
    # return df
    return gdf


def f2(row: gpd.GeoSeries, shorelines, arg1):
    # use data in the row to construct a geodataframe that is used to filter another geodataframe
    buffer = gpd.GeoDataFrame(geometry=[row["geometry"]], crs=row["src_crs"])
    shorelines = gpd.overlay(shorelines, buffer, keep_geom_type=False)

    # apply the f1 function to each row of the resulting GeoDataFrame, result will be point geometries
    result = shorelines.apply(lambda x: f1(x, arg1), axis=1)
    return result


# Sample data for the shorelines and transects GeoDataFrames - both linestrings
shorelines = gpd.GeoDataFrame(
    geometry=[
        shapely.geometry.LineString([(0, 0), (1, 1)]),
        shapely.geometry.LineString([(0, 1), (1, 0)]),
    ],
    crs="EPSG:4326",
)
shorelines["src_crs"] = 4326

transects = gpd.GeoDataFrame(
    geometry=[
        shapely.geometry.LineString([(0.5, 0), (0.5, 1)]),
        shapely.geometry.LineString([(0, 0.5), (1, 0.5)]),
    ],
    crs="EPSG:4326",
)
transects["src_crs"] = 4326

# arg1 is just here to emphasize why we need to wrap the apply functions in a lambda
arg1 = 2
transects.apply(lambda x: f2(x, shorelines, arg1), axis=1)

```

#### Problem description

Cryptic Pandas errors that are time consuming to debug, while handling the data as Pandas dataframe makes it more difficult to keep track of the geospatial attributes. 

#### Expected Output

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.10.9 | packaged by conda-forge | (main, Feb  2 2023, 20:26:08) [Clang 14.0.6 ]
executable : /Users/calkoen/mambaforge/envs/coastal/bin/python
machine    : macOS-13.1-arm64-arm-64bit

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.11.1
GEOS lib   : None
GDAL       : 3.6.2
GDAL data dir: /Users/calkoen/mambaforge/envs/coastal/share/gdal
PROJ       : 9.1.0
PROJ data dir: /Users/calkoen/mambaforge/envs/coastal/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.12.2
numpy      : 1.23.5
pandas     : 1.5.3
pyproj     : 3.4.1
shapely    : 2.0.1
fiona      : 1.9.1
geoalchemy2: 0.13.1
geopy      : 2.3.0
matplotlib : 3.7.0
mapclassify: 2.5.0
pygeos     : 0.14
pyogrio    : 0.5.1
psycopg2   : 2.9.3 (dt dec pq3 ext lo64)
pyarrow    : 10.0.1
rtree      : 1.0.1

</details>



Discussion:
Hey @FlorisCalkoen you're correct there's a geopandas bug here, we should catch a Shapely GeometryTypeError here;
https://github.com/geopandas/geopandas/blob/04c2dee547777d9e87f9df4c85cc372a03b29f93/geopandas/geodataframe.py#L1613 as well.

However what is less clear to me is what you're trying to accomplish here, if I run your example for a dataframe the return type is a dataframe which has rows which are pd.Series which rows are pd.DataFrames with a geometry column. This is certainly not how I would expect anyone to be using geopandas (or pandas for that matter) as with nested values like this most if not all of the series/ dataframe records will not work.

(perhaps this could just be an artefact of your reproducer, and it doesn't feature in your normal workflows)

```python
res = transects.apply(lambda x: f2(x, shorelines, arg1), axis=1)
print("result")
print(res, type(res), res.columns)
print("r1")
r1 = res.iloc[0]
print(r1, type(r1))
print("r11")
r11 = r1.iloc[0]
print(r11, type(r11))
result
                                                   0                                                  1
0        geometry
0  POINT (0 0)
1  POINT (0 1)
2...        geometry
0  POINT (0 0)
1  POINT (0 1)
2...
1        geometry
0  POINT (0 0)
1  POINT (0 1)
2...        geometry
0  POINT (0 0)
1  POINT (0 1)
2... <class 'pandas.core.frame.DataFrame'> RangeIndex(start=0, stop=2, step=1)
r1
0          geometry
0  POINT (0 0)
1  POINT (0 1)
2...
1          geometry
0  POINT (0 0)
1  POINT (0 1)
2...
Name: 0, dtype: object <class 'pandas.core.series.Series'>
r11
      geometry
0  POINT (0 0)
1  POINT (0 1)
2  POINT (1 1)
3  POINT (1 0) <class 'pandas.core.frame.DataFrame'>
```

Resolving patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -3,6 +3,7 @@
 
 import numpy as np
 import pandas as pd
+import shapely.errors
 from pandas import DataFrame, Series
 from pandas.core.accessor import CachedAccessor
 
@@ -19,6 +20,11 @@
 from . import _compat as compat
 from ._decorator import doc
 
+if compat.SHAPELY_GE_18:
+    geometry_type_error = shapely.errors.GeometryTypeError
+else:
+    geometry_type_error = ValueError
+
 
 def _geodataframe_constructor_with_fallback(*args, **kwargs):
     """
@@ -1613,7 +1619,7 @@ def apply(self, func, axis=0, raw=False, result_type=None, args=(), **kwargs):
                     # not enough info about func to preserve CRS
                     result = _ensure_geometry(result)
 
-                except TypeError:
+                except (TypeError, geometry_type_error):
                     pass
 
         return result


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
@@ -752,6 +752,11 @@ def test_df_apply_returning_series(df):
     result = df.apply(lambda x: None, axis=1)
     assert result.dtype == "object"
 
+    # https://github.com/geopandas/geopandas/issues/2889
+    # contrived case such that `from_shapely` receives an array of geodataframes
+    res = df.apply(lambda row: df.geometry.to_frame(), axis=1)
+    assert res.dtype == "object"
+
 
 def test_df_apply_geometry_dtypes(df):
     # https://github.com/geopandas/geopandas/issues/1852