martinfleis: You may have a case when you want your resulting GeoSeries to have no CRS (e.g. transforming from state plane to naive geometry with custom 0,0 for construction purposes), we should allow that. Now you can either pass new one or inherit existing. We should have 3 options:

- preserve CRS
- set specific CRS
- use no CRS

I don't have a strong opinion on the actual keyword, but it should make sense in all three cases, so `crs` might be optimal for that.
jorisvandenbossche: So add `crs=False` ? 
martinfleis: Having `None`, `value`, `False` options? Can we change the default to `True`?
jorisvandenbossche: Hmm, I find `crs=True` a bit strange. `preserve_crs=True/False` seems better, but that then doesn't fully fit with passing a CRS object ..
martinfleis: Yeah, but `crs=None` to preserve and `crs=False` to set CRS to None is also strange. 
martinfleis: What about `apply_crs` as a keyword?
brendan-ward: Another option would be to always preserve the existing CRS if one exists, and let the user set it / clear it properly after `apply`; this seems potentially easiest from an API documentation standpoint (`always preserves existing CRS, if set.`).  Since CRS is set in place within `apply`, there should be low memory churn to setting it after the call.

inplace
```
result = s.apply(lambda x: x.centroid)
result.set_crs(4326, inplace=True)
```

chained
```
result = s.apply(lambda x: x.centroid).set_crs(4326)
```



Otherwise, it seems like the trap of a 3-way parameter remains: preseve existing, override with new, set to `None`
martinfleis: @brendan-ward that is actually quite a good point. I'd go with that.
jorisvandenbossche: That's fine for me as well.

In practice, it will be `s.apply(lambda x: x.centroid).set_crs(4326, allow_override=True)` (with the `allow_override` keyword) if you want to change the preserved CRS though.
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 52eea7c1..ba73e695 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -42,6 +42,24 @@ def _geoseries_constructor_with_fallback(data=None, index=None, crs=None, **kwar
         return Series(data=data, index=index, **kwargs)
 
 
+def inherit_doc(cls):
+    """
+    A decorator adding a docstring from an existing method.
+    """
+
+    def decorator(decorated):
+        original_method = getattr(cls, decorated.__name__, None)
+        if original_method:
+            doc = original_method.__doc__ or ""
+        else:
+            doc = ""
+
+        decorated.__doc__ = doc
+        return decorated
+
+    return decorator
+
+
 class GeoSeries(GeoPandasBase, Series):
     """
     A Series object designed to store shapely geometry objects.
@@ -284,15 +302,26 @@ class GeoSeries(GeoPandasBase, Series):
     def __getitem__(self, key):
         return self._wrapped_pandas_method("__getitem__", key)
 
+    @inherit_doc(pd.Series)
     def sort_index(self, *args, **kwargs):
         return self._wrapped_pandas_method("sort_index", *args, **kwargs)
 
+    @inherit_doc(pd.Series)
     def take(self, *args, **kwargs):
         return self._wrapped_pandas_method("take", *args, **kwargs)
 
+    @inherit_doc(pd.Series)
     def select(self, *args, **kwargs):
         return self._wrapped_pandas_method("select", *args, **kwargs)
 
+    @inherit_doc(pd.Series)
+    def apply(self, func, args=(), **kwargs):
+        result = super().apply(func, args=args, **kwargs)
+        if isinstance(result, GeoSeries):
+            if self.crs is not None:
+                result.set_crs(self.crs, inplace=True)
+        return result
+
     def __finalize__(self, other, method=None, **kwargs):
         """ propagate metadata from other to self """
         # NOTE: backported from pandas master (upcoming v0.13)

ENH: adapt apply() to handle CRS
Following #1339 and discussion there, we should adapt `apply` to preserve CRS if it is applied on GeometryArrays. Once the CRS on GA level is merged. Currently (within the PR yet) any function applied on any other column than `geometry` drops CRS.

As discussed, it should come with the ability to opt-out/assign different CRS.

See https://github.com/geopandas/geopandas/pull/1339#issuecomment-605295675 and https://github.com/geopandas/geopandas/pull/1339#issuecomment-605676457 for details.
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index ec212627..222b3ba2 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -553,6 +553,14 @@ class TestGeometryArrayCRS:
         df2 = df.astype({"col1": str})
         assert df2.crs == self.osgb
 
+    def test_apply(self):
+        s = GeoSeries(self.arr)
+        assert s.crs == 27700
+
+        # apply preserves the CRS if the result is a GeoSeries
+        result = s.apply(lambda x: x.centroid)
+        assert result.crs == 27700
+
 
 class TestSetCRS:
     @pytest.mark.parametrize(
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 7078331f..ed63b36b 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -489,6 +489,26 @@ def test_groupby_groups(df):
     assert_frame_equal(res, exp)
 
 
+def test_apply(s):
+    # function that returns geometry preserves GeoSeries class
+    def geom_func(geom):
+        assert isinstance(geom, Point)
+        return geom
+
+    result = s.apply(geom_func)
+    assert isinstance(result, GeoSeries)
+    assert_geoseries_equal(result, s)
+
+    # function that returns non-geometry results in Series
+    def numeric_func(geom):
+        assert isinstance(geom, Point)
+        return geom.x
+
+    result = s.apply(numeric_func)
+    assert not isinstance(result, GeoSeries)
+    assert_series_equal(result, pd.Series([0.0, 1.0, 2.0]))
+
+
 def test_apply_loc_len1(df):
     # subset of len 1 with loc -> bug in pandas with inconsistent Block ndim
     # resulting in bug in apply

pip install -e .
parse_log_pytest
requirements.txt
pytest descartes matplotlib rtree
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==24.2.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
click==8.1.8
click-plugins==1.1.1
cligj==0.7.2
cycler==0.11.0
descartes==1.1.0
exceptiongroup==1.2.2
fiona==1.9.6
fonttools==4.38.0
-e git+https://github.com/geopandas/geopandas.git@81fbc3cf1d6a60f4171e8ace31b1353783ad6f79#egg=geopandas
importlib-metadata==6.7.0
iniconfig==2.0.0
kiwisolver==1.4.5
matplotlib==3.5.3
numpy==1.21.6
packaging==24.0
pandas==1.3.5
Pillow==9.5.0
pluggy==1.2.0
pyparsing==3.1.4
pyproj==3.2.1
pytest==7.4.4
python-dateutil==2.9.0.post0
pytz==2025.2
Rtree==1.0.1
shapely==2.0.7
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
zipp==3.15.0

name: geopandas
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==24.2.0
      - click==8.1.8
      - click-plugins==1.1.1
      - cligj==0.7.2
      - cycler==0.11.0
      - descartes==1.1.0
      - exceptiongroup==1.2.2
      - fiona==1.9.6
      - fonttools==4.38.0
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - kiwisolver==1.4.5
      - matplotlib==3.5.3
      - numpy==1.21.6
      - packaging==24.0
      - pandas==1.3.5
      - pillow==9.5.0
      - pluggy==1.2.0
      - pyparsing==3.1.4
      - pyproj==3.2.1
      - pytest==7.4.4
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - rtree==1.0.1
      - shapely==2.0.7
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/geopandas

geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_apply
geopandas/tests/test_crs.py::test_to_crs_transform geopandas/tests/test_crs.py::test_to_crs_inplace geopandas/tests/test_crs.py::test_to_crs_geo_column_name geopandas/tests/test_crs.py::test_transform2[epsg_number-epsg_number] geopandas/tests/test_crs.py::test_transform2[epsg_number-epsg_string] geopandas/tests/test_crs.py::test_transform2[epsg_number-epsg_dict] geopandas/tests/test_crs.py::test_transform2[epsg_number-proj4_string] geopandas/tests/test_crs.py::test_transform2[epsg_number-proj4_dict] geopandas/tests/test_crs.py::test_transform2[epsg_string-epsg_number] geopandas/tests/test_crs.py::test_transform2[epsg_string-epsg_string] geopandas/tests/test_crs.py::test_transform2[epsg_string-epsg_dict] geopandas/tests/test_crs.py::test_transform2[epsg_string-proj4_string] geopandas/tests/test_crs.py::test_transform2[epsg_string-proj4_dict] geopandas/tests/test_crs.py::test_transform2[epsg_dict-epsg_number] geopandas/tests/test_crs.py::test_transform2[epsg_dict-epsg_string] geopandas/tests/test_crs.py::test_transform2[epsg_dict-epsg_dict] geopandas/tests/test_crs.py::test_transform2[epsg_dict-proj4_string] geopandas/tests/test_crs.py::test_transform2[epsg_dict-proj4_dict] geopandas/tests/test_crs.py::test_transform2[proj4_string-epsg_number] geopandas/tests/test_crs.py::test_transform2[proj4_string-epsg_string] geopandas/tests/test_crs.py::test_transform2[proj4_string-epsg_dict] geopandas/tests/test_crs.py::test_transform2[proj4_string-proj4_string] geopandas/tests/test_crs.py::test_transform2[proj4_string-proj4_dict] geopandas/tests/test_crs.py::test_transform2[proj4_dict-epsg_number] geopandas/tests/test_crs.py::test_transform2[proj4_dict-epsg_string] geopandas/tests/test_crs.py::test_transform2[proj4_dict-epsg_dict] geopandas/tests/test_crs.py::test_transform2[proj4_dict-proj4_string] geopandas/tests/test_crs.py::test_transform2[proj4_dict-proj4_dict] geopandas/tests/test_crs.py::test_crs_axis_order__always_xy geopandas/tests/test_crs.py::test_skip_exact_same geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_array geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_series geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_dataframe geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_scalar[None] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_scalar[scalar1] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_scalar[scalar2] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_read_file geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_multiple_geoms geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_multiple_geoms_set_geom geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_assign_cols geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_copy geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_rename geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_to_crs geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_from_shapely geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_from_wkb geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_from_wkt geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_points_from_xy geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_original geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_ops geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_binary_ops geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_other geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_affinity_methods[affine_transform-arg0] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_affinity_methods[translate-arg1] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_affinity_methods[rotate-arg2] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_affinity_methods[scale-arg3] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_affinity_methods[skew-arg4] geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_slice geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_concat geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_merge geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_deprecation geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_setitem_geometry geopandas/tests/test_crs.py::TestGeometryArrayCRS::test_astype geopandas/tests/test_crs.py::TestSetCRS::test_set_crs[geoseries] geopandas/tests/test_crs.py::TestSetCRS::test_set_crs[geodataframe] geopandas/tests/test_pandas_methods.py::test_repr geopandas/tests/test_pandas_methods.py::test_repr_boxed_display_precision geopandas/tests/test_pandas_methods.py::test_repr_all_missing geopandas/tests/test_pandas_methods.py::test_repr_empty geopandas/tests/test_pandas_methods.py::test_indexing geopandas/tests/test_pandas_methods.py::test_reindex geopandas/tests/test_pandas_methods.py::test_take geopandas/tests/test_pandas_methods.py::test_take_empty geopandas/tests/test_pandas_methods.py::test_assignment geopandas/tests/test_pandas_methods.py::test_assign geopandas/tests/test_pandas_methods.py::test_astype geopandas/tests/test_pandas_methods.py::test_astype_invalid_geodataframe geopandas/tests/test_pandas_methods.py::test_to_csv geopandas/tests/test_pandas_methods.py::test_numerical_operations geopandas/tests/test_pandas_methods.py::test_where geopandas/tests/test_pandas_methods.py::test_select_dtypes geopandas/tests/test_pandas_methods.py::test_equals geopandas/tests/test_pandas_methods.py::test_fillna geopandas/tests/test_pandas_methods.py::test_dropna geopandas/tests/test_pandas_methods.py::test_isna[None] geopandas/tests/test_pandas_methods.py::test_isna[nan] geopandas/tests/test_pandas_methods.py::test_any_all geopandas/tests/test_pandas_methods.py::test_unique geopandas/tests/test_pandas_methods.py::test_groupby geopandas/tests/test_pandas_methods.py::test_groupby_groups geopandas/tests/test_pandas_methods.py::test_apply geopandas/tests/test_pandas_methods.py::test_apply_loc_len1
geopandas/geoseries.py
geopandas/geoseries.py