Repository: geopandas/geopandas
Pull request #2859 (2023-04-04 12:10:11)

CLN: Remove getitem special error behaviour
Addresses https://github.com/geopandas/geopandas/issues/2517

Based on discussion in #2857, remove the special None handling of geometry column name as the associated error message is less helpful than the equivalent for loc/iloc.

This includes #2857, so will have a much smaller diff after that is merged.

The end goal is to simplify the logic in #2577 by removing `_geometry_column=None` as a possible case.




Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6ac18b0..90e028b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ New features and improvements:
   of the GeoParquet specification (geoparquet.org) (#2663).
 - New ``hilbert_distance()`` method that calculates the distance along a Hilbert curve
   for each geometry in a GeoSeries/GeoDataFrame (#2297).
+- Added support to fill missing values in `GeoSeries.fillna` via another `GeoSeries` (#2535).
 - Support for sorting geometries (for example, using ``sort_values()``) based on
   the distance along the Hilbert curve (#2070).
 - Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataframe (#2621).
@@ -16,6 +17,8 @@ New features and improvements:
 - Added `minimum_bounding_radius()` as GeoSeries method (#2827).
 - Added support for append (``mode="a"`` or ``append=True``) in ``to_file()``
   using ``engine="pyogrio"`` (#2788).
+- Added a ``to_wgs84`` keyword to ``to_json`` allowing automatic re-projecting to follow
+  the 2016 GeoJSON specification (#416).
 
 Deprecations and compatibility notes:
 
@@ -35,7 +38,7 @@ Bug fixes:
 - Fix `to_parquet`/`to_feather` to use correct WKB flavor for 3D geometries (#2654)
 - Fix `read_file` to avoid reading all file bytes prior to calling Fiona or
   Pyogrio if provided a URL as input (#2796)
-- Fix `copy()` downcasting GeoDataFrames without an active geometry column to a 
+- Fix `copy()` downcasting GeoDataFrames without an active geometry column to a
   DataFrame (#2775)
 
 Notes on (optional) dependencies:
diff --git a/doc/source/gallery/create_geopandas_from_pandas.ipynb b/doc/source/gallery/create_geopandas_from_pandas.ipynb
index 4907e276..a8f9f86c 100644
--- a/doc/source/gallery/create_geopandas_from_pandas.ipynb
+++ b/doc/source/gallery/create_geopandas_from_pandas.ipynb
@@ -62,8 +62,10 @@
     "``points_from_xy()`` to transform **Longitude** and **Latitude** into a list\n",
     "of ``shapely.Point`` objects and set it as a ``geometry`` while creating the\n",
     "``GeoDataFrame``. (note that ``points_from_xy()`` is an enhanced wrapper for\n",
-    "``[Point(x, y) for x, y in zip(df.Longitude, df.Latitude)]``)\n",
-    "\n"
+    "``[Point(x, y) for x, y in zip(df.Longitude, df.Latitude)]``). The ``crs``\n",
+    "value is also set to explicitly state the geometry data defines latitude/\n",
+    "longitude world geodetic degree values. This is important for the correct \n",
+    "interpretation of the data, such as when plotting with data in other formats."
    ]
   },
   {
@@ -73,7 +75,7 @@
    "outputs": [],
    "source": [
     "gdf = geopandas.GeoDataFrame(\n",
-    "    df, geometry=geopandas.points_from_xy(df.Longitude, df.Latitude)\n",
+    "    df, geometry=geopandas.points_from_xy(df.Longitude, df.Latitude), crs=\"EPSG:4326\"\n",
     ")"
    ]
   },
diff --git a/geopandas/array.py b/geopandas/array.py
index 1474ddf6..6a52b20a 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1009,34 +1009,46 @@ class GeometryArray(ExtensionArray):
         return GeometryArray(result, crs=self.crs)
 
     def _fill(self, idx, value):
-        """Fill index locations with value
+        """
+        Fill index locations with ``value``.
 
-        Value should be a BaseGeometry
+        ``value`` should be a BaseGeometry or a GeometryArray.
         """
-        if not (_is_scalar_geometry(value) or value is None):
+        if vectorized.isna(value):
+            value = [None]
+        elif _is_scalar_geometry(value):
+            value = [value]
+        elif isinstance(value, GeometryArray):
+            value = value[idx]
+        else:
             raise TypeError(
-                "Value should be either a BaseGeometry or None, got %s" % str(value)
+                "'value' parameter must be None, a scalar geometry, or a GeoSeries, "
+                f"but you passed a {type(value).__name__!r}"
             )
-        # self._data[idx] = value
-        value_arr = np.empty(1, dtype=object)
+
+        value_arr = np.empty(len(value), dtype=object)
         with compat.ignore_shapely2_warnings():
-            value_arr[:] = [value]
+            value_arr[:] = _shapely_to_geom(value)
+
         self._data[idx] = value_arr
         return self
 
     def fillna(self, value=None, method=None, limit=None):
-        """Fill NA/NaN values using the specified method.
+        """
+        Fill NA values with geometry (or geometries) or using the specified method.
 
         Parameters
         ----------
-        value : scalar, array-like
-            If a scalar value is passed it is used to fill all missing values.
-            Alternatively, an array-like 'value' can be given. It's expected
-            that the array-like have the same length as 'self'.
+        value : shapely geometry object or GeometryArray
+            If a geometry value is passed it is used to fill all missing values.
+            Alternatively, an GeometryArray 'value' can be given. It's expected
+            that the GeometryArray has the same length as 'self'.
+
         method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None
             Method to use for filling holes in reindexed Series
             pad / ffill: propagate last valid observation forward to next valid
             backfill / bfill: use NEXT valid observation to fill gap
+
         limit : int, default None
             If method is specified, this is the maximum number of consecutive
             NaN values to forward/backward fill. In other words, if there is
@@ -1047,26 +1059,14 @@ class GeometryArray(ExtensionArray):
 
         Returns
         -------
-        filled : ExtensionArray with NA/NaN filled
+        GeometryArray
         """
         if method is not None:
             raise NotImplementedError("fillna with a method is not yet supported")
 
         mask = self.isna()
         new_values = self.copy()
-
-        if mask.any():
-            # fill with value
-            if vectorized.isna(value):
-                value = None
-            elif not isinstance(value, BaseGeometry):
-                raise NotImplementedError(
-                    "fillna currently only supports filling with a scalar geometry"
-                )
-            value = _shapely_to_geom(value)
-            new_values = new_values._fill(mask, value)
-
-        return new_values
+        return new_values._fill(mask, value) if mask.any() else new_values
 
     def astype(self, dtype, copy=True):
         """
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 887506d3..0169dd49 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -732,7 +732,9 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
 
         return df
 
-    def to_json(self, na="null", show_bbox=False, drop_id=False, **kwargs):
+    def to_json(
+        self, na="null", show_bbox=False, drop_id=False, to_wgs84=False, **kwargs
+    ):
         """
         Returns a GeoJSON representation of the ``GeoDataFrame`` as a string.
 
@@ -747,6 +749,12 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             Whether to retain the index of the GeoDataFrame as the id property
             in the generated GeoJSON. Default is False, but may want True
             if the index is just arbitrary row numbers.
+        to_wgs84: bool, optional, default: False
+            If the CRS is set on the active geometry column it is exported as
+            WGS84 (EPSG:4326) to meet the `2016 GeoJSON specification
+            <https://tools.ietf.org/html/rfc7946>`_.
+            Set to True to force re-projection and set to False to ignore CRS. False by
+            default.
 
         Notes
         -----
@@ -785,8 +793,17 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         GeoDataFrame.to_file : write GeoDataFrame to file
 
         """
+        if to_wgs84:
+            if self.crs:
+                df = self.to_crs(epsg=4326)
+            else:
+                raise ValueError(
+                    "CRS is not set. Cannot re-project to WGS84 (EPSG:4326)."
+                )
+        else:
+            df = self
         return json.dumps(
-            self._to_geo(na=na, show_bbox=show_bbox, drop_id=drop_id), **kwargs
+            df._to_geo(na=na, show_bbox=show_bbox, drop_id=drop_id), **kwargs
         )
 
     @property
@@ -1435,8 +1452,6 @@ individually so that features may have different properties
                 result.__class__ = GeoDataFrame
                 if geo_col in result:
                     result._geometry_column_name = geo_col
-                else:
-                    result._geometry_column_name = None
             else:
                 result.__class__ = DataFrame
         return result
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 175704c3..3c651e8e 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -751,11 +751,27 @@ class GeoSeries(GeoPandasBase, Series):
         return self.notna()
 
     def fillna(self, value=None, method=None, inplace=False, **kwargs):
-        """Fill NA values with a geometry (empty polygon by default).
+        """
+        Fill NA values with geometry (or geometries).
+
+        ``method`` is currently not implemented.
+
+        Parameters
+        ----------
+        value : shapely geometry or GeoSeries, default None
+            If None is passed, NA values will be filled with GEOMETRYCOLLECTION EMPTY.
+            If a shapely geometry object is passed, it will be
+            used to fill all missing values. If a ``GeoSeries`` or ``GeometryArray``
+            are passed, missing values will be filled based on the corresponding index
+            locations. If pd.NA or np.nan are passed, values will be filled with
+            ``None`` (not GEOMETRYCOLLECTION EMPTY).
+
+        Returns
+        -------
+        GeoSeries
 
         Examples
         --------
-
         >>> from shapely.geometry import Polygon
         >>> s = geopandas.GeoSeries(
         ...     [
@@ -770,18 +786,38 @@ class GeoSeries(GeoPandasBase, Series):
         2    POLYGON ((0.00000 0.00000, -1.00000 1.00000, 0...
         dtype: geometry
 
+        Filled with an empty polygon.
+
         >>> s.fillna()
         0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
         1                             GEOMETRYCOLLECTION EMPTY
         2    POLYGON ((0.00000 0.00000, -1.00000 1.00000, 0...
         dtype: geometry
 
+        Filled with a specific polygon.
+
         >>> s.fillna(Polygon([(0, 1), (2, 1), (1, 2)]))
         0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
         1    POLYGON ((0.00000 1.00000, 2.00000 1.00000, 1....
         2    POLYGON ((0.00000 0.00000, -1.00000 1.00000, 0...
         dtype: geometry
 
+        Filled with another GeoSeries.
+
+        >>> from shapely.geometry import Point
+        >>> s_fill = geopandas.GeoSeries(
+        ...     [
+        ...         Point(0, 0),
+        ...         Point(1, 1),
+        ...         Point(2, 2),
+        ...     ]
+        ... )
+        >>> s.fillna(s_fill)
+        0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
+        1                              POINT (1.00000 1.00000)
+        2    POLYGON ((0.00000 0.00000, -1.00000 1.00000, 0...
+        dtype: geometry
+
         See Also
         --------
         GeoSeries.isna : detect missing values


Test patch:
diff --git a/geopandas/tests/test_extension_array.py b/geopandas/tests/test_extension_array.py
index 50914058..dddbcaf3 100644
--- a/geopandas/tests/test_extension_array.py
+++ b/geopandas/tests/test_extension_array.py
@@ -367,19 +367,40 @@ class TestMissing(extension_tests.BaseMissingTests):
         fill_value = data_missing[1]
         ser = pd.Series(data_missing)
 
+        # Fill with a scalar
         result = ser.fillna(fill_value)
         expected = pd.Series(data_missing._from_sequence([fill_value, fill_value]))
         self.assert_series_equal(result, expected)
 
-        # filling with array-like not yet supported
-
-        # # Fill with a series
-        # result = ser.fillna(expected)
-        # self.assert_series_equal(result, expected)
+        # Fill with a series
+        filler = pd.Series(
+            from_shapely(
+                [
+                    shapely.geometry.Point(1, 1),
+                    shapely.geometry.Point(2, 2),
+                ],
+            )
+        )
+        result = ser.fillna(filler)
+        expected = pd.Series(data_missing._from_sequence([fill_value, fill_value]))
+        self.assert_series_equal(result, expected)
 
-        # # Fill with a series not affecting the missing values
-        # result = ser.fillna(ser)
-        # self.assert_series_equal(result, ser)
+        # Fill with a series not affecting the missing values
+        filler = pd.Series(
+            from_shapely(
+                [
+                    shapely.geometry.Point(2, 2),
+                    shapely.geometry.Point(1, 1),
+                ]
+            ),
+            index=[10, 11],
+        )
+        result = ser.fillna(filler)
+        self.assert_series_equal(result, ser)
+
+        # More `GeoSeries.fillna` testcases are in
+        # `geopandas\tests\test_pandas_methods.py::test_fillna_scalar`
+        # and `geopandas\tests\test_pandas_methods.py::test_fillna_series`.
 
     @pytest.mark.skip("fillna method not supported")
     def test_fillna_limit_pad(self, data_missing):
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 73e8463e..b7ffaa48 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -342,7 +342,6 @@ class TestDataFrame:
     def test_get_geometry_invalid(self):
         df = GeoDataFrame()
         df["geom"] = self.df.geometry
-        msg_geo_col_none = "active geometry column to use has not been set. "
         msg_geo_col_missing = "is not present. "
 
         with pytest.raises(AttributeError, match=msg_geo_col_missing):
@@ -350,7 +349,7 @@ class TestDataFrame:
         df2 = self.df.copy()
         df2["geom2"] = df2.geometry
         df2 = df2[["BoroCode", "BoroName", "geom2"]]
-        with pytest.raises(AttributeError, match=msg_geo_col_none):
+        with pytest.raises(AttributeError, match=msg_geo_col_missing):
             df2.geometry
 
         msg_other_geo_cols_present = "There are columns with geometry data type"
@@ -429,12 +428,28 @@ class TestDataFrame:
         assert_frame_equal(res2, exp2_nogeom)
 
     def test_to_json(self):
-        text = self.df.to_json()
+        text = self.df.to_json(to_wgs84=True)
         data = json.loads(text)
         assert data["type"] == "FeatureCollection"
         assert len(data["features"]) == 5
         assert "id" in data["features"][0].keys()
 
+        # check it converts to WGS84
+        coord = data["features"][0]["geometry"]["coordinates"][0][0][0]
+        np.testing.assert_allclose(coord, [-74.0505080640324, 40.5664220341941])
+
+    def test_to_json_wgs84_false(self):
+        text = self.df.to_json()
+        data = json.loads(text)
+        # check it doesn't convert to WGS84
+        coord = data["features"][0]["geometry"]["coordinates"][0][0][0]
+        assert coord == [970217.0223999023, 145643.33221435547]
+
+    def test_to_json_no_crs(self):
+        self.df.crs = None
+        with pytest.raises(ValueError, match="CRS is not set"):
+            self.df.to_json(to_wgs84=True)
+
     @pytest.mark.filterwarnings(
         "ignore:Geometry column does not contain geometry:UserWarning"
     )
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 7bebc84b..5bc71237 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -1,4 +1,5 @@
 import string
+import sys
 import warnings
 
 import numpy as np
@@ -1016,6 +1017,10 @@ class TestGeomMethods:
         with pytest.warns(FutureWarning, match="Currently, index_parts defaults"):
             assert_geoseries_equal(expected, s.explode())
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     @pytest.mark.parametrize("index_name", [None, "test"])
     def test_explode_geodataframe(self, index_name):
         s = GeoSeries([MultiPoint([Point(1, 2), Point(2, 3)]), Point(5, 5)])
@@ -1035,6 +1040,10 @@ class TestGeomMethods:
         expected_df = expected_df.set_index(expected_index)
         assert_frame_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     @pytest.mark.parametrize("index_name", [None, "test"])
     def test_explode_geodataframe_level_1(self, index_name):
         # GH1393
@@ -1125,6 +1134,10 @@ class TestGeomMethods:
         exploded_df = gdf.explode(column="col1", ignore_index=True)
         assert_geodataframe_equal(exploded_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     @pytest.mark.parametrize("outer_index", [1, (1, 2), "1"])
     def test_explode_pandas_multi_index(self, outer_index):
         index = MultiIndex.from_arrays(
@@ -1232,6 +1245,10 @@ class TestGeomMethods:
         test_df = df.explode(ignore_index=True, index_parts=True)
         assert_frame_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     def test_explode_order(self):
         df = GeoDataFrame(
             {"vals": [1, 2, 3]},
@@ -1261,6 +1278,10 @@ class TestGeomMethods:
         )
         assert_geodataframe_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     def test_explode_order_no_multi(self):
         df = GeoDataFrame(
             {"vals": [1, 2, 3]},
@@ -1279,6 +1300,10 @@ class TestGeomMethods:
         )
         assert_geodataframe_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     def test_explode_order_mixed(self):
         df = GeoDataFrame(
             {"vals": [1, 2, 3]},
@@ -1307,6 +1332,10 @@ class TestGeomMethods:
         )
         assert_geodataframe_equal(test_df, expected_df)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     def test_explode_duplicated_index(self):
         df = GeoDataFrame(
             {"vals": [1, 2, 3]},
@@ -1429,6 +1458,10 @@ class TestGeomMethods:
         )
         assert_frame_equal(self.g11.get_coordinates(ignore_index=True), expected)
 
+    @pytest.mark.skipif(
+        sys.platform == "win32" and sys.version_info < (3, 9),
+        reason="Inconsistent int dtype",
+    )
     @pytest.mark.skipif(
         not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
         reason="get_coordinates not implemented for shapely<2",
diff --git a/geopandas/tests/test_op_output_types.py b/geopandas/tests/test_op_output_types.py
index ea2ed500..365d3627 100644
--- a/geopandas/tests/test_op_output_types.py
+++ b/geopandas/tests/test_op_output_types.py
@@ -120,8 +120,8 @@ def test_getitem(df):
     assert_object(df[["value1", "value2"]], pd.DataFrame)
     assert_object(df[[geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df[[geo_name]], GeoDataFrame, geo_name)
-    assert_obj_no_active_geo_col(df[["geometry2", "value1"]], GeoDataFrame)
-    assert_obj_no_active_geo_col(df[["geometry2"]], GeoDataFrame)
+    assert_obj_no_active_geo_col(df[["geometry2", "value1"]], GeoDataFrame, geo_name)
+    assert_obj_no_active_geo_col(df[["geometry2"]], GeoDataFrame, geo_name)
     assert_object(df[["value1"]], pd.DataFrame)
     # Series
     assert_object(df[geo_name], GeoSeries, geo_name)
@@ -134,13 +134,10 @@ def test_loc(df):
     assert_object(df.loc[:, ["value1", "value2"]], pd.DataFrame)
     assert_object(df.loc[:, [geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df.loc[:, [geo_name]], GeoDataFrame, geo_name)
-    # These two are inconsistent with getitem, active geom col dropped,
-    # but other geometry columns present
     assert_obj_no_active_geo_col(
         df.loc[:, ["geometry2", "value1"]], GeoDataFrame, geo_name
     )
     assert_obj_no_active_geo_col(df.loc[:, ["geometry2"]], GeoDataFrame, geo_name)
-    # #####
     assert_object(df.loc[:, ["value1"]], pd.DataFrame)
     # Series
     assert_object(df.loc[:, geo_name], GeoSeries, geo_name)
@@ -153,11 +150,8 @@ def test_iloc(df):
     assert_object(df.iloc[:, 0:2], pd.DataFrame)
     assert_object(df.iloc[:, 2:4], GeoDataFrame, geo_name)
     assert_object(df.iloc[:, [2]], GeoDataFrame, geo_name)
-    # These two are inconsistent with getitem, active geom col dropped,
-    # but other geometry columns present
     assert_obj_no_active_geo_col(df.iloc[:, [3, 0]], GeoDataFrame, geo_name)
     assert_obj_no_active_geo_col(df.iloc[:, [3]], GeoDataFrame, geo_name)
-    # #####
     assert_object(df.iloc[:, [0]], pd.DataFrame)
     # Series
     assert_object(df.iloc[:, 2], GeoSeries, geo_name)
@@ -189,15 +183,12 @@ def test_reindex(df):
     assert_object(df.reindex(columns=[geo_name, "geometry2"]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=[geo_name]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=["new_col", geo_name]), GeoDataFrame, geo_name)
-    # These two are inconsistent with getitem, active geom col dropped,
-    # but other geometry columns present
     assert_obj_no_active_geo_col(
         df.reindex(columns=["geometry2", "value1"]), GeoDataFrame, geo_name
     )
     assert_obj_no_active_geo_col(
         df.reindex(columns=["geometry2"]), GeoDataFrame, geo_name
     )
-    # #####
     assert_object(df.reindex(columns=["value1"]), pd.DataFrame)
 
     # reindexing the rows always preserves the GeoDataFrame
@@ -216,15 +207,12 @@ def test_drop(df):
     assert_object(df.drop(columns=["value1", "value2"]), GeoDataFrame, geo_name)
     cols = ["value1", "value2", "geometry2"]
     assert_object(df.drop(columns=cols), GeoDataFrame, geo_name)
-    # These two are inconsistent with getitem, active geom col dropped,
-    # but other geometry columns present
     assert_obj_no_active_geo_col(
         df.drop(columns=[geo_name, "value2"]), GeoDataFrame, geo_name
     )
     assert_obj_no_active_geo_col(
         df.drop(columns=["value1", "value2", geo_name]), GeoDataFrame, geo_name
     )
-    # #####
     assert_object(df.drop(columns=["geometry2", "value2", geo_name]), pd.DataFrame)
 
 
@@ -238,10 +226,10 @@ def test_apply(df):
     assert_object(df[["value1", "value2"]].apply(identity), pd.DataFrame)
     assert_object(df[[geo_name, "geometry2"]].apply(identity), GeoDataFrame, geo_name)
     assert_object(df[[geo_name]].apply(identity), GeoDataFrame, geo_name)
-    expected_geo_col_name = None if compat.PANDAS_GE_14 else "geometry"
-    assert_obj_no_active_geo_col(
-        df[["geometry2", "value1"]].apply(identity), GeoDataFrame, expected_geo_col_name
-    )
+    expected_geo_col_name = geo_name if compat.PANDAS_GE_14 else "geometry"
+
+    res = df[["geometry2", "value1"]].apply(identity)
+    assert_obj_no_active_geo_col(res, GeoDataFrame, expected_geo_col_name)
     assert_obj_no_active_geo_col(
         df[["geometry2"]].apply(identity), GeoDataFrame, expected_geo_col_name
     )
@@ -272,12 +260,13 @@ def test_apply(df):
 
 
 def test_apply_axis1_secondary_geo_cols(df):
+    geo_name = df.geometry.name if compat.PANDAS_GE_14 else "geometry"
+
     def identity(x):
         return x
 
-    expected_geo_col_name = None if compat.PANDAS_GE_14 else "geometry"
     assert_obj_no_active_geo_col(
-        df[["geometry2"]].apply(identity, axis=1), GeoDataFrame, expected_geo_col_name
+        df[["geometry2"]].apply(identity, axis=1), GeoDataFrame, geo_name
     )
 
 
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 0373b551..99da2185 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -384,8 +384,9 @@ def test_equals(s, df):
 # Missing values
 
 
-def test_fillna(s, df):
+def test_fillna_scalar(s, df):
     s2 = GeoSeries([Point(0, 0), None, Point(2, 2)])
+
     res = s2.fillna(Point(1, 1))
     assert_geoseries_equal(res, s)
 
@@ -410,6 +411,28 @@ def test_fillna(s, df):
     assert_geodataframe_equal(res.astype({"value1": "int64"}), df)
 
 
+def test_fillna_series(s):
+    # fill na with another GeoSeries
+    s2 = GeoSeries([Point(0, 0), None, Point(2, 2)])
+
+    # check na filled with the same index
+    res = s2.fillna(GeoSeries([Point(1, 1)] * 3))
+    assert_geoseries_equal(res, s)
+
+    # check na filled based on index, not position
+    index = [3, 2, 1]
+    res = s2.fillna(GeoSeries([Point(i, i) for i in index], index=index))
+    assert_geoseries_equal(res, s)
+
+    # check na filled but the input length is different
+    res = s2.fillna(GeoSeries([Point(1, 1)], index=[1]))
+    assert_geoseries_equal(res, s)
+
+    # check na filled but the inputting index is different
+    res = s2.fillna(GeoSeries([Point(1, 1)], index=[9]))
+    assert_geoseries_equal(res, s2)
+
+
 def test_dropna():
     s2 = GeoSeries([Point(0, 0), None, Point(2, 2)])
     res = s2.dropna()