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:
m-richards: > This is a weird pattern...
> 
> The fix is okay I guess, I would just suggest users not to get there :).

Agree and Agree, but we can still fix it regardless. 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bf10b95..14041dff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,9 +16,12 @@ New features and improvements:
 - Added ``exclusive`` parameter to ``sjoin_nearest`` method for Shapely >= 2.0 (#2877)
 
 Bug fixes:
+- Fix ambiguous error when GeoDataFrame is initialised with a column called "crs" (#2944)
 
 - Fix a color assignment in ``explore`` when using ``UserDefined`` bins (#2923)
 - ``assert_geodataframe_equal`` now handles GeoDataFrames with no active geometry (#2498)
+- Fix bug in `apply` with `axis=1` where the given user defined function returns nested 
+  data in the geometry column (#2959)
 
 ## Version 0.13.2 (Jun 6, 2023)
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index ed4a3606..41991a7f 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -3,6 +3,7 @@ import warnings
 
 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 geopandas.explore import _explore
 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):
     """
@@ -1529,7 +1535,10 @@ individually so that features may have different properties
             if pd.api.types.is_scalar(value) or isinstance(value, BaseGeometry):
                 value = [value] * self.shape[0]
             try:
-                crs = getattr(self, "crs", None)
+                if self._geometry_column_name is not None:
+                    crs = getattr(self, "crs", None)
+                else:  # don't use getattr, because a col "crs" might exist
+                    crs = None
                 value = _ensure_geometry(value, crs=crs)
                 if key == "geometry":
                     self._persist_old_default_geometry_colname()
@@ -1613,7 +1622,7 @@ individually so that features may have different properties
                     # 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_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 009cdbc8..8dcc9381 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1469,3 +1469,11 @@ def test_geodataframe_crs_nonrepresentable_json(crs):
     ):
         gdf_geojson = json.loads(gdf.to_json())
     assert "crs" not in gdf_geojson
+
+
+def test_geodataframe_crs_colname():
+    # https://github.com/geopandas/geopandas/issues/2942
+    gdf = GeoDataFrame({"crs": [1], "geometry": [Point(1, 1)]})
+    assert gdf.crs is None
+    assert gdf["crs"].iloc[0] == 1
+    assert getattr(gdf, "crs") is None
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index c10a4183..2f569b4d 100644
--- 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