Repository: geopandas/geopandas

Issue:
BUG: pd.concat crs warning when geometry column is missing from one of the arguments
@martinfleis thanks for the quick reply, it took me a while but I think I found the problem, it's not directly related I guess but it comes from my use of ``sjoin_nearest`` with saved geometries.

The shortest MWE I could come up with was:

```python
import osmnx as ox
import pandas as pd

area = ox.geocode_to_gdf("Ivry-sur-Seine, France").iloc[0].geometry

landuse = ox.geometries_from_polygon(area, tags={"landuse": "commercial"})

buildings = ox.geometries_from_polygon(area, tags={"building": "retail"})

buildings["savedgeom"] = buildings.geometry

nearest = landuse.to_crs('+proj=cea').sjoin_nearest(buildings.to_crs('+proj=cea'))

nearest["geometry"] = nearest["savedgeom"]
del nearest["savedgeom"]

nearest = nearest.to_crs(landuse.crs)

print(buildings.crs, nearest.crs)

pd.concat([nearest, buildings])
```

_Originally posted by @tfardet in https://github.com/geopandas/geopandas/issues/2606#issuecomment-1325571377_
 
---------------------------------------
I've just moved this to a separate issue so it is easier to track / refer to as it does describe a bug.
A minimal reproducer is
```python
import pandas as pd

import geopandas as gpd
gdf = gpd.read_file(gpd.datasets.get_path("nybb"))
print(gdf.crs)
gdf = gdf.assign(geom=gdf.geometry)
pd.concat([gdf[['geometry']],gdf[['geom', 'geometry']]] )

```

Discussion:

Patch:
diff --git a/ci/envs/39-pd12-conda-forge.yaml b/ci/envs/39-pd12-conda-forge.yaml
index fc675bf9..73b62ec4 100644
--- a/ci/envs/39-pd12-conda-forge.yaml
+++ b/ci/envs/39-pd12-conda-forge.yaml
@@ -5,6 +5,7 @@ dependencies:
   - python=3.9
   # required
   - pandas=1.2
+  - numpy=1.20 # released 1 month after pandas 1.2
   - shapely
   - fiona
   - pyproj
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index 01f6fb6c..d1f132b4 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -20,6 +20,7 @@ PANDAS_GE_115 = Version(pd.__version__) >= Version("1.1.5")
 PANDAS_GE_12 = Version(pd.__version__) >= Version("1.2.0")
 PANDAS_GE_13 = Version(pd.__version__) >= Version("1.3.0")
 PANDAS_GE_14 = Version(pd.__version__) >= Version("1.4.0rc0")
+PANDAS_GE_20 = Version(pd.__version__) >= Version("2.0.0.dev0")
 
 
 # -----------------------------------------------------------------------------
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index b1700b49..20aa7332 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -83,6 +83,9 @@ def _pygeos_to_shapely(geom):
     if pygeos.is_empty(geom) and pygeos.get_type_id(geom) == 0:
         # empty point does not roundtrip through WKB
         return shapely.wkt.loads("POINT EMPTY")
+    elif pygeos.get_type_id(geom) == 2:
+        # linearring does not roundtrip through WKB
+        return shapely.LinearRing(shapely.wkb.loads(pygeos.to_wkb(geom)))
     else:
         return shapely.wkb.loads(pygeos.to_wkb(geom))
 
diff --git a/geopandas/array.py b/geopandas/array.py
index 9f366612..5f472333 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1394,6 +1394,9 @@ class GeometryArray(ExtensionArray):
 
 
 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]
diff --git a/geopandas/base.py b/geopandas/base.py
index 70c40bda..ada4c79d 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -308,7 +308,7 @@ GeometryCollection
         >>> gdf = geopandas.GeoDataFrame(d, crs="EPSG:4326")
         >>> gdf
                            geometry
-        0  GEOMETRYCOLLECTION EMPTY
+        0               POINT EMPTY
         1   POINT (2.00000 1.00000)
         2                      None
         >>> gdf.is_empty
@@ -2247,7 +2247,7 @@ GeometryCollection
         0                                                 None
         1    POLYGON ((0.00000 2.00000, 2.00000 2.00000, 1....
         2    MULTILINESTRING ((0.00000 0.00000, 1.00000 1.0...
-        3                                     LINESTRING EMPTY
+        3                                   LINESTRING Z EMPTY
         4                              POINT (0.00000 1.00000)
         5                                                 None
         dtype: geometry
@@ -2362,7 +2362,7 @@ GeometryCollection
         0                                                 None
         1    POLYGON ((0.00000 2.00000, 2.00000 2.00000, 1....
         2    MULTILINESTRING ((0.00000 0.00000, 1.00000 1.0...
-        3                                     LINESTRING EMPTY
+        3                                   LINESTRING Z EMPTY
         4        MULTIPOINT (0.00000 1.00000, 1.00000 1.00000)
         5                                                 None
         dtype: geometry
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index a7a84ee7..ef850187 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -673,7 +673,7 @@ class GeoSeries(GeoPandasBase, Series):
         >>> s
         0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
         1                                                 None
-        2                             GEOMETRYCOLLECTION EMPTY
+        2                                        POLYGON EMPTY
         dtype: geometry
         >>> s.isna()
         0    False
@@ -717,7 +717,7 @@ class GeoSeries(GeoPandasBase, Series):
         >>> s
         0    POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0....
         1                                                 None
-        2                             GEOMETRYCOLLECTION EMPTY
+        2                                        POLYGON EMPTY
         dtype: geometry
         >>> s.notna()
         0     True


Test patch:
diff --git a/ci/envs/39-latest-conda-forge.yaml b/ci/envs/39-latest-conda-forge.yaml
index 6058f0b9..0dfc4b9c 100644
--- a/ci/envs/39-latest-conda-forge.yaml
+++ b/ci/envs/39-latest-conda-forge.yaml
@@ -1,12 +1,13 @@
 name: test
 channels:
   - conda-forge
-  # - conda-forge/label/shapely_dev
 dependencies:
   - python=3.9
   # required
   - pandas=1.3
-  # - shapely=2
+  - shapely=2
+  - geos
+  - cython
   - fiona
   - pyproj
   # use this build to have one with only shapely 2.0 installed
@@ -34,7 +35,3 @@ dependencies:
   - pyarrow
   # doctest testing
   - pytest-doctestplus
-  - pip
-  - pip:
-      - --pre
-      - shapely==2.0b1
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index df616a5e..416935d9 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -16,6 +16,7 @@ from shapely.geometry import Point, Polygon, box
 
 import geopandas
 from geopandas import GeoDataFrame, read_file
+from geopandas._compat import PANDAS_GE_20
 from geopandas.io.file import _detect_driver, _EXTENSION_TO_DRIVER
 
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
@@ -309,7 +310,11 @@ def test_read_file_datetime_mixed_offsets(tmpdir):
     if FIONA_GE_1814:
         # Convert mixed timezones to UTC equivalent
         assert is_datetime64_any_dtype(res["date"])
-        assert res["date"].dt.tz == pytz.utc
+        if not PANDAS_GE_20:
+            utc = pytz.utc
+        else:
+            utc = datetime.timezone.utc
+        assert res["date"].dt.tz == utc
     else:
         # old fiona and pyogrio ignore timezones and read as datetimes successfully
         assert is_datetime64_any_dtype(res["date"])
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
index 3f65346a..f048b9a6 100644
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -1,3 +1,5 @@
+import warnings
+
 import pandas as pd
 import pytest
 from geopandas.testing import assert_geodataframe_equal
@@ -103,6 +105,39 @@ class TestMerging:
                 [self.gdf, self.gdf.set_crs("epsg:4326"), self.gdf.set_crs("epsg:4327")]
             )
 
+    def test_concat_axis0_unaligned_cols(self):
+        # https://github.com/geopandas/geopandas/issues/2679
+        gdf = self.gdf.set_crs("epsg:4326").assign(
+            geom=self.gdf.geometry.set_crs("epsg:4327")
+        )
+        both_geom_cols = gdf[["geom", "geometry"]]
+        single_geom_col = gdf[["geometry"]]
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            pd.concat([both_geom_cols, single_geom_col])
+        # Check order of mismatch doesn't matter
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            pd.concat([single_geom_col, both_geom_cols])
+
+        # Side effect of this fix, explicitly provided all none geoseries
+        # will not be warned for (ideally this would still warn)
+        explicit_all_none_case = gdf[["geometry"]].assign(
+            geom=GeoSeries([None for _ in range(len(gdf))])
+        )
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            pd.concat([both_geom_cols, explicit_all_none_case])
+
+        # Check concat with partially None col is not affected by the special casing
+        # for all None no CRS handling
+        with pytest.warns(
+            UserWarning, match=r"CRS not set for some of the concatenation inputs.*"
+        ):
+            partial_none_case = self.gdf[["geometry"]]
+            partial_none_case.iloc[0] = None
+            pd.concat([single_geom_col, partial_none_case])
+
     def test_concat_axis1(self):
 
         res = pd.concat([self.gdf, self.df], axis=1)
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index b29ce854..27d45f1d 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -5,7 +5,7 @@ from numpy.testing import assert_array_equal
 import pandas as pd
 
 import shapely
-from shapely.geometry import Point, GeometryCollection, LineString
+from shapely.geometry import Point, GeometryCollection, LineString, LinearRing
 
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries
@@ -86,6 +86,24 @@ def test_repr_empty():
     assert "geometry" in df._repr_html_()
 
 
+def test_repr_linearring():
+    # https://github.com/geopandas/geopandas/pull/2689
+    # specifically, checking internal shapely/pygeos/wkt/wkb conversions
+    # preserve LinearRing
+    s = GeoSeries([LinearRing([(0, 0), (1, 1), (1, -1)])])
+    assert "LINEARRING" in str(s.iloc[0])  # shapely scalar repr
+    assert "LINEARRING" in str(s)  # GeoSeries repr
+
+    # check something coercible to linearring is not converted
+    s2 = GeoSeries(
+        [
+            LineString([(0, 0), (1, 1), (1, -1)]),
+            LineString([(0, 0), (1, 1), (1, -1), (0, 0)]),
+        ]
+    )
+    assert "LINEARRING" not in str(s2)
+
+
 def test_indexing(s, df):
 
     # accessing scalar from the geometry (column)
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
index c3406dce..91a71650 100644
--- a/geopandas/tests/test_sindex.py
+++ b/geopandas/tests/test_sindex.py
@@ -11,7 +11,6 @@ from shapely.geometry import (
 from numpy.testing import assert_array_equal
 
 import geopandas
-import pandas as pd
 from geopandas import _compat as compat
 from geopandas import GeoDataFrame, GeoSeries, read_file, datasets
 
@@ -76,13 +75,15 @@ class TestSeriesSindex:
         s = GeoSeries([t1, t2, sq])
         assert s.sindex.size == 3
 
+    @pytest.mark.filterwarnings("ignore:The series.append method is deprecated")
+    @pytest.mark.skipif(compat.PANDAS_GE_20, reason="append removed in pandas 2.0")
     def test_polygons_append(self):
         t1 = Polygon([(0, 0), (1, 0), (1, 1)])
         t2 = Polygon([(0, 0), (1, 1), (0, 1)])
         sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
         s = GeoSeries([t1, t2, sq])
         t = GeoSeries([t1, t2, sq], [3, 4, 5])
-        s = pd.concat([s, t])
+        s = s.append(t)
         assert len(s) == 6
         assert s.sindex.size == 6