Repository: geopandas/geopandas

Issue:
BUG: GeoDataFrame.merge incorrectly returning dataframe when geometry columns are present.
Originally discussed in #2931, 

A merge which retains geometry columns, but loses the active geometry column (as in the below) should return a geodataframe to follow consistent downcasting rules with the same case with `loc`/`drop` or `concat`
```python
import geopandas
import shapely
point = shapely.Point(600000, 5000000)
df_1 = geopandas.GeoDataFrame(data={"geom": [shapely.Point([1, 2])]}, geometry="geom", crs="25832")
df_merged = df_1.merge(df_1, left_index=True, right_index=True)
assert type(df_merged) == geopandas.GeoDataFrame # returns a dataframe instead
# Expect this to be the same, but this assertion passes:
res = pd.concat([df_1.geom.rename("geom_x"), df_1.geom.rename("geom_y")], axis=1)
assert type(res) == geopandas.GeoDataFrame
```


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 815ddac1..53d92f5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ Notes on dependencies:
   rtree package has also been removed. (#3035)
 
 
+Bug fixes:
+- Fix `GeoDataFrame.merge()` incorrectly returning a `DataFrame` instead of a 
+  `GeoDataFrame` when the `suffixes` argument is applied to the active
+  geometry column (#2933).
+
+
 ## Version 0.14 (Sep 15, 2023)
 
 GeoPandas will use Shapely 2.0 by default instead of PyGEOS when both Shapely >= 2.0 and
@@ -41,7 +47,6 @@ New features and improvements:
   for files with the `.fgb` extension (#2958)
 
 Bug fixes:
-
 - Fix ambiguous error when GeoDataFrame is initialized with a column called ``"crs"`` (#2944)
 - Fix a color assignment in ``explore`` when using ``UserDefined`` bins (#2923)
 - Fix bug in `apply` with `axis=1` where the given user defined function returns nested
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 1be99545..e7a6e4f0 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1550,34 +1550,6 @@ individually so that features may have different properties
             copied._geometry_column_name = self._geometry_column_name
         return copied
 
-    def merge(self, *args, **kwargs):
-        r"""Merge two ``GeoDataFrame`` objects with a database-style join.
-
-        Returns a ``GeoDataFrame`` if a geometry column is present; otherwise,
-        returns a pandas ``DataFrame``.
-
-        Returns
-        -------
-        GeoDataFrame or DataFrame
-
-        Notes
-        -----
-        The extra arguments ``*args`` and keyword arguments ``**kwargs`` are
-        passed to DataFrame.merge.
-        See https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas\
-.DataFrame.merge.html
-        for more details.
-        """
-        result = DataFrame.merge(self, *args, **kwargs)
-        geo_col = self._geometry_column_name
-        if isinstance(result, DataFrame) and geo_col in result:
-            result.__class__ = GeoDataFrame
-            result.crs = self.crs
-            result._geometry_column_name = geo_col
-        elif isinstance(result, DataFrame) and geo_col not in result:
-            result.__class__ = DataFrame
-        return result
-
     @doc(pd.DataFrame)
     def apply(self, func, axis=0, raw=False, result_type=None, args=(), **kwargs):
         result = super().apply(
@@ -1946,15 +1918,13 @@ individually so that features may have different properties
         return df
 
     # overrides the pandas astype method to ensure the correct return type
+    # should be removable when pandas 1.4 is dropped
     def astype(self, dtype, copy=True, errors="raise", **kwargs):
         """
         Cast a pandas object to a specified dtype ``dtype``.
-
         Returns a GeoDataFrame when the geometry column is kept as geometries,
         otherwise returns a pandas DataFrame.
-
         See the pandas.DataFrame.astype docstring for more details.
-
         Returns
         -------
         GeoDataFrame or DataFrame
@@ -1971,29 +1941,6 @@ individually so that features may have different properties
         # do not return a GeoDataFrame
         return pd.DataFrame(df)
 
-    def convert_dtypes(self, *args, **kwargs):
-        """
-        Convert columns to best possible dtypes using dtypes supporting ``pd.NA``.
-
-        Always returns a GeoDataFrame as no conversions are applied to the
-        geometry column.
-
-        See the pandas.DataFrame.convert_dtypes docstring for more details.
-
-        Returns
-        -------
-        GeoDataFrame
-
-        """
-        # Overridden to fix GH1870, that return type is not preserved always
-        # (and where it was, geometry col was not)
-
-        return GeoDataFrame(
-            super().convert_dtypes(*args, **kwargs),
-            geometry=self.geometry.name,
-            crs=self.crs,
-        )
-
     def to_postgis(
         self,
         name,


Test patch:
diff --git a/geopandas/tests/test_op_output_types.py b/geopandas/tests/test_op_output_types.py
index 74a1587e..2d6b37c8 100644
--- a/geopandas/tests/test_op_output_types.py
+++ b/geopandas/tests/test_op_output_types.py
@@ -6,7 +6,7 @@ from shapely.geometry import Point
 import numpy as np
 
 from geopandas import GeoDataFrame, GeoSeries
-
+from geopandas.testing import assert_geodataframe_equal
 
 crs_osgb = pyproj.CRS(27700)
 crs_wgs = pyproj.CRS(4326)
@@ -370,3 +370,13 @@ def test_constructor_sliced_in_pandas_methods(df2):
     assert type(hashable_test_df.duplicated()) == pd.Series
     assert type(df2.quantile(numeric_only=True)) == pd.Series
     assert type(df2.memory_usage()) == pd.Series
+
+
+def test_merge_preserve_geodataframe():
+    # https://github.com/geopandas/geopandas/issues/2932
+    ser = GeoSeries.from_xy([1], [1])
+    df = GeoDataFrame({"geo": ser})
+    res = df.merge(df, left_index=True, right_index=True)
+    assert_obj_no_active_geo_col(res, GeoDataFrame, geo_colname=None)
+    expected = GeoDataFrame({"geo_x": ser, "geo_y": ser})
+    assert_geodataframe_equal(expected, res)