Repository: geopandas/geopandas
Pull request #3023 (2023-09-21 12:58:09)

BUG: Don't use set in concat CRS checking
quick test to fix issue discussed in #326 which probably should be its own issue.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6b2106b..1f0c63be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ 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).
+- Fix bug in `pandas.concat` CRS consistency checking where CRS differing by WKT 
+  whitespace only were treated as incompatible (#3023)
 
 ## Version 0.14.1 (Nov 11, 2023)
 
diff --git a/geopandas/array.py b/geopandas/array.py
index e22ee9d7..c83ff5e0 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1593,15 +1593,20 @@ def _get_common_crs(arr_seq):
     # mask out all None arrays with no crs (most likely auto generated by pandas
     # from concat with missing column)
     arr_seq = [ga for ga in arr_seq if not (ga.isna().all() and ga.crs is None)]
-
-    crs_set = {arr.crs for arr in arr_seq}
-    crs_not_none = [crs for crs in crs_set if crs is not None]
+    # determine unique crs without using a set, because CRS hash can be different
+    # for objects with the same CRS
+    unique_crs = []
+    for arr in arr_seq:
+        if arr.crs not in unique_crs:
+            unique_crs.append(arr.crs)
+
+    crs_not_none = [crs for crs in unique_crs if crs is not None]
     names = [crs.name for crs in crs_not_none]
 
     if len(crs_not_none) == 0:
         return None
     if len(crs_not_none) == 1:
-        if len(crs_set) != 1:
+        if len(unique_crs) != 1:
             warnings.warn(
                 "CRS not set for some of the concatenation inputs. "
                 f"Setting output's CRS as {names[0]} "
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index e0cdf905..44db62ed 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -152,8 +152,6 @@ class GeoSeries(GeoPandasBase, Series):
 
     """
 
-    _metadata = ["name"]
-
     def __init__(self, data=None, index=None, crs: Optional[Any] = None, **kwargs):
         if hasattr(data, "crs") and crs:
             if not data.crs:
@@ -619,6 +617,14 @@ class GeoSeries(GeoPandasBase, Series):
     def _constructor(self):
         return _geoseries_constructor_with_fallback
 
+    def _constructor_from_mgr(self, mgr, axes):
+        assert isinstance(mgr, SingleBlockManager)
+
+        if not isinstance(mgr.blocks[0].dtype, GeometryDtype):
+            return Series._from_mgr(mgr, axes)
+
+        return GeoSeries._from_mgr(mgr, axes)
+
     @property
     def _constructor_expanddim(self):
         return _geoseries_expanddim


Test patch:
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
index 2ae1c9f8..7832f1f4 100644
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -1,6 +1,7 @@
 import warnings
 
 import pandas as pd
+import pyproj
 import pytest
 from geopandas.testing import assert_geodataframe_equal
 from pandas.testing import assert_index_equal
@@ -133,6 +134,38 @@ class TestMerging:
             partial_none_case.iloc[0] = None
             pd.concat([single_geom_col, partial_none_case])
 
+    def test_concat_axis0_crs_wkt_mismatch(self):
+        # https://github.com/geopandas/geopandas/issues/326#issuecomment-1727958475
+        wkt_template = """GEOGCRS["WGS 84",
+        ENSEMBLE["World Geodetic System 1984 ensemble",
+        MEMBER["World Geodetic System 1984 (Transit)"],
+        MEMBER["World Geodetic System 1984 (G730)"],
+        MEMBER["World Geodetic System 1984 (G873)"],
+        MEMBER["World Geodetic System 1984 (G1150)"],
+        MEMBER["World Geodetic System 1984 (G1674)"],
+        MEMBER["World Geodetic System 1984 (G1762)"],
+        MEMBER["World Geodetic System 1984 (G2139)"],
+        ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],
+        ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,
+        ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],
+        AXIS["geodetic latitude (Lat)",north,ORDER[1],
+        ANGLEUNIT["degree",0.0174532925199433]],
+        AXIS["geodetic longitude (Lon)",east,ORDER[2],
+        ANGLEUNIT["degree",0.0174532925199433]],
+        USAGE[SCOPE["Horizontal component of 3D system."],
+        AREA["World.{}"],BBOX[-90,-180,90,180]],ID["EPSG",4326]]"""
+        wkt_v1 = wkt_template.format("")
+        wkt_v2 = wkt_template.format(" ")  # add additional whitespace
+        crs1 = pyproj.CRS.from_wkt(wkt_v1)
+        crs2 = pyproj.CRS.from_wkt(wkt_v2)
+        # pyproj crs __hash__ based on WKT strings means these are distinct in a
+        # set are but equal by equality
+        assert len({crs1, crs2}) == 2
+        assert crs1 == crs2
+        expected = pd.concat([self.gdf, self.gdf]).set_crs(crs1)
+        res = pd.concat([self.gdf.set_crs(crs1), self.gdf.set_crs(crs2)])
+        assert_geodataframe_equal(expected, res)
+
     def test_concat_axis1(self):
         res = pd.concat([self.gdf, self.df], axis=1)