Repository: geopandas/geopandas

Issue:
GeoDataFrame explode with non-default active geometry name
I'm running into an issue when calling explode on a geodataframe with an active geometry named something other than "geometry". After looking through the source code, this seems to be because the active geometry is renamed to "geometry"; however, `_geometry_column_name` does not get updated.

Example:
```py
gdf = GeoDataFrame({"geom": [MultiPoint([(1, 2), (3, 4)])]}, geometry="geom", crs=4326)

# All good
print(gdf.geometry.name)
print(gdf._geometry_column_name)
print(gdf.columns)

gdf = gdf.explode(ignore_index=True)

# gdf.geometry.name != gdf._geometry_column_name
print(gdf.geometry.name)
print(gdf._geometry_column_name)
print(gdf.columns)

gdf.to_crs(crs=gdf.estimate_utm_crs(), inplace=True)

# gdf now has an unwanted geom column
print(gdf.geometry.name)
print(gdf._geometry_column_name)
print(gdf.columns)
```
A workaround is to call `gdf.set_geometry(gdf.geometry.name, inplace=True)` after the call to explode.

Discussion:
martinfleis: The CI is failing on a reported codecov issue (https://github.com/codecov/codecov-action/issues/788), not the tests.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3dc849d..45c904fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,19 +8,32 @@ New features and improvements:
 
 Deprecations and compatibility notes:
 
--   The `version` parameter for the `to_feather` and `to_parquet` methods has
+- The `version` parameter for the `to_feather` and `to_parquet` methods has
     been replaced with `schema_version`.  `version` will be passed directly to
     underlying feather or parquet writer. `version` will only be used to set
     `schema_version` if `version` is one of 0.1.0 or 0.4.0 (#2496).
 
 Bug fixes:
+- Fix a crash in datetime column reading where the file contains mixed timezone
+  offsets (#2479). These will be read as UTC localized values.
 
 - Fix regression (RecursionError) in reshape methods such as ``unstack()``
   and ``pivot()`` involving MultiIndex, or GeoDataFrame construction with
   MultiIndex (#2486).
+- Fix regression in ``GeoDataFrame.explode()`` with non-default
+  geometry column name.
 
 Notes on (optional) dependencies:
 
+Version 0.11.1 (July xx, 2022)
+---------------------------------
+
+Small bug-fix release:
+
+- Fix regression in ``apply()`` causing row-wise all nan float columns to be 
+  casted to GeometryDtype (#2482). 
+
+
 Version 0.11 (June 20, 2022)
 ----------------------------
 
@@ -141,7 +154,6 @@ Notes on (optional) dependencies:
   dependencies have now changed to shapely 1.7, fiona 1.8.13.post1, pyproj 2.6.1.post1,
   matplotlib 3.2, mapclassify 2.4.0 (#2358, #2391)
 
-
 Version 0.10.2 (October 16, 2021)
 ---------------------------------
 
@@ -161,7 +173,6 @@ Small bug-fix release:
 - Fix ``unary_union`` to correctly handle a GeoSeries with missing values (#2181).
 - Avoid internal deprecation warning in ``clip()`` (#2179).
 
-
 Version 0.10.1 (October 8, 2021)
 --------------------------------
 
@@ -170,7 +181,6 @@ Small bug-fix release:
 - Fix regression in ``overlay()`` with non-overlapping geometries and a
   non-default ``how`` (i.e. not "intersection") (#2157).
 
-
 Version 0.10.0 (October 3, 2021)
 --------------------------------
 
@@ -207,7 +217,7 @@ New features and improvements:
 - Improved heuristic to decide how many decimals to show in the repr based on
   whether the CRS is projected or geographic (#1895).
 - Switched the default for ``geocode()`` from GeoCode.Farm to the Photon
-  geocoding API (https://photon.komoot.io) (#2007).
+  geocoding API (<https://photon.komoot.io>) (#2007).
 
 Deprecations and compatibility notes:
 
@@ -257,7 +267,6 @@ Notes on (optional) dependencies:
 - Compatibility fixes for the latest PyGEOS (#1872, #2014) and matplotlib
   (colorbar issue, #2066).
 
-
 Version 0.9.0 (February 28, 2021)
 ---------------------------------
 
@@ -374,13 +383,11 @@ Notes on (optional) dependencies:
   is still a default requirement) (#1775).
 - Compatibility with the upcoming Shapely 1.8 (#1659, #1662, #1819).
 
-
 Version 0.8.2 (January 25, 2021)
 --------------------------------
 
 Small bug-fix release for compatibility with PyGEOS 0.9.
 
-
 Version 0.8.1 (July 15, 2020)
 -----------------------------
 
@@ -392,7 +399,6 @@ Small bug-fix release:
 - Fix the un-pickling with ``pd.read_pickle`` of files written with older
   GeoPandas versions (#1511).
 
-
 Version 0.8.0 (June 24, 2020)
 -----------------------------
 
@@ -491,7 +497,6 @@ And we now have a [Code of Conduct](https://github.com/geopandas/geopandas/blob/
 GeoPandas 0.8.0 is the last release to support Python 3.5. The next release
 will require Python 3.6, pandas 0.24, numpy 1.15 and shapely 1.6 or higher.
 
-
 Version 0.7.0 (February 16, 2020)
 ---------------------------------
 
@@ -536,7 +541,6 @@ Bug fixes:
 - Fixed the ``geopandas.sjoin`` function to handle MultiIndex correctly (#1159).
 - Fixed the ``geopandas.sjoin`` function to preserve the index name of the left GeoDataFrame (#1150).
 
-
 Version 0.6.3 (February 6, 2020)
 ---------------------------------
 
@@ -547,7 +551,6 @@ Small bug-fix release:
   no missing values in the geometry column. This should make it easier to fill
   the numerical columns of the GeoDataFrame (#1279).
 
-
 Version 0.6.2 (November 18, 2019)
 ---------------------------------
 
@@ -560,7 +563,6 @@ Small bug-fix release fixing a few regressions:
 - Fix filtering of a GeoDataFrame to preserve the index type when ending up
   with an empty result (#1190).
 
-
 Version 0.6.1 (October 12, 2019)
 --------------------------------
 
@@ -569,7 +571,6 @@ Small bug-fix release fixing a few regressions:
 - Fix ``astype`` when converting to string with Multi geometries (#1145) or when converting a dataframe without geometries (#1144).
 - Fix ``GeoSeries.fillna`` to accept ``np.nan`` again (#1149).
 
-
 Version 0.6.0 (September 27, 2019)
 ----------------------------------
 
@@ -610,10 +611,8 @@ Bug fixes:
 - Fixed ``GeoDataFrame.to_file`` to preserve VFS file paths (e.g. when a "s3://" path is specified) (#1124).
 - Fixed failing case in ``geopandas.sjoin`` with empty geometries (#1138).
 
-
 In addition, the minimum required versions of some dependencies have been increased: GeoPandas now requirs pandas >=0.23.4 and matplotlib >=2.0.1 (#1002).
 
-
 Version 0.5.1 (July 11, 2019)
 -----------------------------
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 92dc5045..a563c905 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1524,14 +1524,17 @@ individually so that features may have different properties
             else:
                 if self.crs is not None and result.crs is None:
                     result.set_crs(self.crs, inplace=True)
-        elif isinstance(result, Series):
-            # Reconstruct series GeometryDtype if lost by apply
-            try:
-                # Note CRS cannot be preserved in this case as func could refer
-                # to any column
-                result = _ensure_geometry(result)
-            except TypeError:
-                pass
+        elif isinstance(result, Series) and result.dtype == "object":
+            # Try reconstruct series GeometryDtype if lost by apply
+            # If all none and object dtype assert list of nones is more likely
+            # intended than list of null geometry.
+            if not result.isna().all():
+                try:
+                    # not enough info about func to preserve CRS
+                    result = _ensure_geometry(result)
+
+                except TypeError:
+                    pass
 
         return result
 
@@ -1825,12 +1828,11 @@ individually so that features may have different properties
 
         exploded_geom = self.geometry.reset_index(drop=True).explode(index_parts=True)
 
-        df = GeoDataFrame(
-            self.drop(self._geometry_column_name, axis=1).take(
-                exploded_geom.index.droplevel(-1)
-            ),
-            geometry=exploded_geom.values,
-        ).__finalize__(self)
+        df = self.drop(self._geometry_column_name, axis=1).take(
+            exploded_geom.index.droplevel(-1)
+        )
+        df[exploded_geom.name] = exploded_geom.values
+        df = df.set_geometry(self._geometry_column_name).__finalize__(self)
 
         if ignore_index:
             df.reset_index(inplace=True, drop=True)
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 8c1783fc..3b4336e8 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -341,9 +341,15 @@ def _read_file_fiona(
                     f_filt, crs=crs, columns=columns + ["geometry"]
                 )
             for k in datetime_fields:
-                # fiona only supports up to ms precision, any microseconds are
-                # floating point rounding error
-                df[k] = pd.to_datetime(df[k]).dt.round(freq="ms")
+                as_dt = pd.to_datetime(df[k])
+                # if to_datetime failed, try again for mixed timezone offsets
+                if as_dt.dtype == "object":
+                    # This can fail if there are invalid datetimes
+                    as_dt = pd.to_datetime(df[k], utc=True)
+                # if to_datetime succeeded, round datetimes as
+                # fiona only supports up to ms precision
+                if not (as_dt.dtype == "object"):
+                    df[k] = as_dt.dt.round(freq="ms")
             return df
 
 


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index fcb9e54b..6ef58582 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -10,6 +10,7 @@ import numpy as np
 import pandas as pd
 
 import pytz
+from pandas.api.types import is_datetime64_any_dtype
 from pandas.testing import assert_series_equal
 from shapely.geometry import Point, Polygon, box
 
@@ -231,6 +232,28 @@ def test_to_file_datetime(tmpdir, driver, ext, time, engine):
         assert_series_equal(df["b"], df_read["b"])
 
 
+def test_read_file_mixed_datetimes(tmpdir):
+    tempfilename = os.path.join(str(tmpdir), "test_mixed_datetime.geojson")
+    df = GeoDataFrame(
+        {
+            "date": [
+                "2014-08-26 10:01:23.040001+02:00",
+                "2019-03-07 17:31:43.118999+01:00",
+            ],
+            "geometry": [Point(1, 1), Point(1, 1)],
+        }
+    )
+    df.to_file(tempfilename)
+    res = read_file(tempfilename)  # check mixed tz don't crash GH2478
+    assert is_datetime64_any_dtype(res["date"])
+    if FIONA_GE_1814:
+        # Convert mixed timezones to UTC equivalent
+        assert res["date"].dt.tz == pytz.utc
+    else:
+        # old fiona and pyogrio ignore timezones and read as datetimes successfully
+        assert is_datetime64_any_dtype(res["date"])
+
+
 @pytest.mark.parametrize("driver,ext", driver_ext_pairs)
 def test_to_file_with_point_z(tmpdir, ext, driver, engine):
     """Test that 3D geometries are retained in writes (GH #612)."""
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 013dc45e..fdd0e3e8 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -1220,6 +1220,24 @@ class TestGeomMethods:
         )
         assert_geodataframe_equal(test_df, expected_df)
 
+    @pytest.mark.parametrize("geom_col", ["geom", "geometry"])
+    def test_explode_geometry_name(self, geom_col):
+        s = GeoSeries([MultiPoint([Point(1, 2), Point(2, 3)]), Point(5, 5)])
+        df = GeoDataFrame({"col": [1, 2], geom_col: s}, geometry=geom_col)
+        test_df = df.explode(index_parts=True)
+
+        assert test_df.geometry.name == geom_col
+        assert test_df.geometry.name == test_df._geometry_column_name
+
+    def test_explode_geometry_name_two_geoms(self):
+        s = GeoSeries([MultiPoint([Point(1, 2), Point(2, 3)]), Point(5, 5)])
+        df = GeoDataFrame({"col": [1, 2], "geom": s, "geometry": s}, geometry="geom")
+        test_df = df.explode(index_parts=True)
+
+        assert test_df.geometry.name == "geom"
+        assert test_df.geometry.name == test_df._geometry_column_name
+        assert "geometry" in test_df.columns
+
     #
     # Test '&', '|', '^', and '-'
     #
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 678b5a09..552d53bd 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -655,6 +655,12 @@ def test_df_apply_returning_series(df):
 
     result = df.apply(lambda row: row.value1, axis=1)
     assert_series_equal(result, df["value1"].rename(None))
+    # https://github.com/geopandas/geopandas/issues/2480
+    result = df.apply(lambda x: float("NaN"), axis=1)
+    assert result.dtype == "float64"
+    # assert list of nones is not promoted to GeometryDtype
+    result = df.apply(lambda x: None, axis=1)
+    assert result.dtype == "object"
 
 
 def test_pivot(df):