Repository: geopandas/geopandas
Pull request #2494 (2022-07-14 15:29:17)

DOC: Reformat plotting_with_folium.ipynb
The formatting of [Plotting with Folium](https://geopandas.org/en/latest/gallery/plotting_with_folium.html) is a bit inconsistent.
Thus, I reformatted the file with [jblack](https://github.com/jupyter-black/jblack).

I can also reformat other Jupyter Notebooks with `jblack` if it makes sense in your opinion.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74a277ff..2fd3f56a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,30 @@ Bug fixes:
 
 Notes on (optional) dependencies:
 
+Version 0.11.1 (July 24, 2022)
+------------------------------
+
+Small bug-fix release:
+
+- 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.
+- Fix regression in ``apply()`` causing row-wise all nan float columns to be 
+  casted to GeometryDtype (#2482). 
+- Fix a crash in datetime column reading where the file contains mixed timezone
+  offsets (#2479). These will be read as UTC localized values.
+- Fix a crash in datetime column reading where the file contains datetimes
+  outside the range supported by [ns] precision (#2505).
+- Fix regression in passing the Parquet or Feather format ``version`` in
+  ``to_parquet`` and ``to_feather``. As a result, the ``version`` parameter
+  for the ``to_parquet`` and ``to_feather`` 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).
+
+
 Version 0.11 (June 20, 2022)
 ----------------------------
 
@@ -132,7 +156,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)
 ---------------------------------
 
@@ -152,7 +175,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)
 --------------------------------
 
@@ -161,7 +183,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)
 --------------------------------
 
@@ -198,7 +219,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:
 
@@ -248,7 +269,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)
 ---------------------------------
 
@@ -365,13 +385,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)
 -----------------------------
 
@@ -383,7 +401,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)
 -----------------------------
 
@@ -482,7 +499,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)
 ---------------------------------
 
@@ -527,7 +543,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)
 ---------------------------------
 
@@ -538,7 +553,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)
 ---------------------------------
 
@@ -551,7 +565,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)
 --------------------------------
 
@@ -560,7 +573,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)
 ----------------------------------
 
@@ -601,10 +613,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/doc/source/gallery/plotting_with_folium.ipynb b/doc/source/gallery/plotting_with_folium.ipynb
index e01574f4..773d6b8a 100644
--- a/doc/source/gallery/plotting_with_folium.ipynb
+++ b/doc/source/gallery/plotting_with_folium.ipynb
@@ -40,7 +40,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "df1 = pd.read_csv('volcano_data_2010.csv')\n",
+    "df1 = pd.read_csv(\"volcano_data_2010.csv\")\n",
     "\n",
     "# Keep only relevant columns\n",
     "df = df1.loc[:, (\"Year\", \"Name\", \"Country\", \"Latitude\", \"Longitude\", \"Type\")]\n",
@@ -55,7 +55,9 @@
    "source": [
     "# Create point geometries\n",
     "geometry = geopandas.points_from_xy(df.Longitude, df.Latitude)\n",
-    "geo_df = geopandas.GeoDataFrame(df[['Year','Name','Country', 'Latitude', 'Longitude', 'Type']], geometry=geometry)\n",
+    "geo_df = geopandas.GeoDataFrame(\n",
+    "    df[[\"Year\", \"Name\", \"Country\", \"Latitude\", \"Longitude\", \"Type\"]], geometry=geometry\n",
+    ")\n",
     "\n",
     "geo_df.head()"
    ]
@@ -66,7 +68,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))\n",
+    "world = geopandas.read_file(geopandas.datasets.get_path(\"naturalearth_lowres\"))\n",
     "df.Type.unique()"
    ]
   },
@@ -80,10 +82,10 @@
    },
    "outputs": [],
    "source": [
-    "fig, ax = plt.subplots(figsize=(24,18))\n",
-    "world.plot(ax=ax, alpha=0.4, color='grey')\n",
-    "geo_df.plot(column='Type', ax=ax, legend=True)\n",
-    "plt.title('Volcanoes')"
+    "fig, ax = plt.subplots(figsize=(24, 18))\n",
+    "world.plot(ax=ax, alpha=0.4, color=\"grey\")\n",
+    "geo_df.plot(column=\"Type\", ax=ax, legend=True)\n",
+    "plt.title(\"Volcanoes\")"
    ]
   },
   {
@@ -101,7 +103,7 @@
    "outputs": [],
    "source": [
     "# Stamen Terrain\n",
-    "map = folium.Map(location = [13.406,80.110], tiles = \"Stamen Terrain\", zoom_start = 9)\n",
+    "map = folium.Map(location=[13.406, 80.110], tiles=\"Stamen Terrain\", zoom_start=9)\n",
     "map"
    ]
   },
@@ -112,7 +114,7 @@
    "outputs": [],
    "source": [
     "# OpenStreetMap\n",
-    "map = folium.Map(location = [13.406,80.110], tiles='OpenStreetMap' , zoom_start = 9)\n",
+    "map = folium.Map(location=[13.406, 80.110], tiles=\"OpenStreetMap\", zoom_start=9)\n",
     "map"
    ]
   },
@@ -123,7 +125,7 @@
    "outputs": [],
    "source": [
     "# Stamen Toner\n",
-    "map = folium.Map(location = [13.406,80.110], tiles='Stamen Toner', zoom_start = 9)\n",
+    "map = folium.Map(location=[13.406, 80.110], tiles=\"Stamen Toner\", zoom_start=9)\n",
     "map"
    ]
   },
@@ -141,7 +143,7 @@
    "outputs": [],
    "source": [
     "# Use terrain map layer to see volcano terrain\n",
-    "map = folium.Map(location = [4,10], tiles = \"Stamen Terrain\", zoom_start = 3)"
+    "map = folium.Map(location=[4, 10], tiles=\"Stamen Terrain\", zoom_start=3)"
    ]
   },
   {
@@ -159,12 +161,12 @@
    "outputs": [],
    "source": [
     "# Create a geometry list from the GeoDataFrame\n",
-    "geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in geo_df.geometry ]\n",
+    "geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in geo_df.geometry]\n",
     "\n",
     "# Iterate through list and add a marker for each volcano, color-coded by its type.\n",
     "i = 0\n",
     "for coordinates in geo_df_list:\n",
-    "    #assign a color marker for the type of volcano, Strato being the most common\n",
+    "    # assign a color marker for the type of volcano, Strato being the most common\n",
     "    if geo_df.Type[i] == \"Stratovolcano\":\n",
     "        type_color = \"green\"\n",
     "    elif geo_df.Type[i] == \"Complex volcano\":\n",
@@ -176,16 +178,19 @@
     "    else:\n",
     "        type_color = \"purple\"\n",
     "\n",
-    "\n",
     "    # Place the markers with the popup labels and data\n",
-    "    map.add_child(folium.Marker(location = coordinates,\n",
-    "                            popup =\n",
-    "                            \"Year: \" + str(geo_df.Year[i]) + '<br>' +\n",
-    "                            \"Name: \" + str(geo_df.Name[i]) + '<br>' +\n",
-    "                            \"Country: \" + str(geo_df.Country[i]) + '<br>'\n",
-    "                            \"Type: \" + str(geo_df.Type[i]) + '<br>'\n",
-    "                            \"Coordinates: \" + str(geo_df_list[i]),\n",
-    "                            icon = folium.Icon(color = \"%s\" % type_color)))\n",
+    "    map.add_child(\n",
+    "        folium.Marker(\n",
+    "            location=coordinates,\n",
+    "            popup=\n",
+    "                \"Year: \" + str(geo_df.Year[i]) + \"<br>\"\n",
+    "                + \"Name: \" + str(geo_df.Name[i]) + \"<br>\"\n",
+    "                + \"Country: \" + str(geo_df.Country[i]) + \"<br>\"\n",
+    "                + \"Type: \" + str(geo_df.Type[i]) + \"<br>\"\n",
+    "                + \"Coordinates: \" + str(geo_df_list[i]),\n",
+    "            icon=folium.Icon(color=\"%s\" % type_color),\n",
+    "        )\n",
+    "    )\n",
     "    i = i + 1"
    ]
   },
@@ -218,9 +223,9 @@
     "\n",
     "from folium import plugins\n",
     "\n",
-    "map = folium.Map(location = [15,30], tiles='Cartodb dark_matter', zoom_start = 2)\n",
+    "map = folium.Map(location=[15, 30], tiles=\"Cartodb dark_matter\", zoom_start=2)\n",
     "\n",
-    "heat_data = [[point.xy[1][0], point.xy[0][0]] for point in geo_df.geometry ]\n",
+    "heat_data = [[point.xy[1][0], point.xy[0][0]] for point in geo_df.geometry]\n",
     "\n",
     "heat_data\n",
     "plugins.HeatMap(heat_data).add_to(map)\n",
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 2b4bbe30..a563c905 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -148,7 +148,11 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             if crs is not None and data.crs != crs:
                 raise ValueError(crs_mismatch_error)
 
-        if geometry is None and "geometry" in self.columns:
+        if (
+            geometry is None
+            and self.columns.nlevels == 1
+            and "geometry" in self.columns
+        ):
             # Check for multiple columns with name "geometry". If there are,
             # self["geometry"] is a gdf and constructor gets recursively recalled
             # by pandas internals trying to access this
@@ -996,7 +1000,7 @@ individually so that features may have different properties
         return df
 
     def to_parquet(
-        self, path, index=None, compression="snappy", version=None, **kwargs
+        self, path, index=None, compression="snappy", schema_version=None, **kwargs
     ):
         """Write a GeoDataFrame to the Parquet format.
 
@@ -1026,7 +1030,7 @@ individually so that features may have different properties
             output except `RangeIndex` which is stored as metadata only.
         compression : {'snappy', 'gzip', 'brotli', None}, default 'snappy'
             Name of the compression to use. Use ``None`` for no compression.
-        version : {'0.1.0', '0.4.0', None}
+        schema_version : {'0.1.0', '0.4.0', None}
             GeoParquet specification version; if not provided will default to
             latest supported version.
         kwargs
@@ -1056,10 +1060,17 @@ individually so that features may have different properties
         from geopandas.io.arrow import _to_parquet
 
         _to_parquet(
-            self, path, compression=compression, index=index, version=version, **kwargs
+            self,
+            path,
+            compression=compression,
+            index=index,
+            schema_version=schema_version,
+            **kwargs,
         )
 
-    def to_feather(self, path, index=None, compression=None, version=None, **kwargs):
+    def to_feather(
+        self, path, index=None, compression=None, schema_version=None, **kwargs
+    ):
         """Write a GeoDataFrame to the Feather format.
 
         Any geometry columns present are serialized to WKB format in the file.
@@ -1089,7 +1100,7 @@ individually so that features may have different properties
         compression : {'zstd', 'lz4', 'uncompressed'}, optional
             Name of the compression to use. Use ``"uncompressed"`` for no
             compression. By default uses LZ4 if available, otherwise uncompressed.
-        version : {'0.1.0', '0.4.0', None}
+        schema_version : {'0.1.0', '0.4.0', None}
             GeoParquet specification version; if not provided will default to
             latest supported version.
         kwargs
@@ -1110,7 +1121,12 @@ individually so that features may have different properties
         from geopandas.io.arrow import _to_feather
 
         _to_feather(
-            self, path, index=index, compression=compression, version=version, **kwargs
+            self,
+            path,
+            index=index,
+            compression=compression,
+            schema_version=schema_version,
+            **kwargs,
         )
 
     def to_file(self, filename, driver=None, schema=None, index=None, **kwargs):
@@ -1508,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
 
@@ -1809,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/arrow.py b/geopandas/io/arrow.py
index 3e443f2d..fea924cf 100644
--- a/geopandas/io/arrow.py
+++ b/geopandas/io/arrow.py
@@ -66,13 +66,13 @@ def _remove_id_from_member_of_ensembles(json_dict):
                 member.pop("id", None)
 
 
-def _create_metadata(df, version=None):
+def _create_metadata(df, schema_version=None):
     """Create and encode geo metadata dict.
 
     Parameters
     ----------
     df : GeoDataFrame
-    version : {'0.1.0', '0.4.0', None}
+    schema_version : {'0.1.0', '0.4.0', None}
         GeoParquet specification version; if not provided will default to
         latest supported version.
 
@@ -81,10 +81,12 @@ def _create_metadata(df, version=None):
     dict
     """
 
-    version = version or METADATA_VERSION
+    schema_version = schema_version or METADATA_VERSION
 
-    if version not in SUPPORTED_VERSIONS:
-        raise ValueError(f"version must be one of: {', '.join(SUPPORTED_VERSIONS)}")
+    if schema_version not in SUPPORTED_VERSIONS:
+        raise ValueError(
+            f"schema_version must be one of: {', '.join(SUPPORTED_VERSIONS)}"
+        )
 
     # Construct metadata for each geometry
     column_metadata = {}
@@ -94,7 +96,7 @@ def _create_metadata(df, version=None):
 
         crs = None
         if series.crs:
-            if version == "0.1.0":
+            if schema_version == "0.1.0":
                 crs = series.crs.to_wkt()
             else:  # version >= 0.4.0
                 crs = series.crs.to_json_dict()
@@ -112,7 +114,7 @@ def _create_metadata(df, version=None):
     return {
         "primary_column": df._geometry_column_name,
         "columns": column_metadata,
-        "version": METADATA_VERSION,
+        "version": schema_version or METADATA_VERSION,
         "creator": {"library": "geopandas", "version": geopandas.__version__},
     }
 
@@ -224,7 +226,7 @@ def _validate_metadata(metadata):
             raise ValueError("Only WKB geometry encoding is supported")
 
 
-def _geopandas_to_arrow(df, index=None, version=None):
+def _geopandas_to_arrow(df, index=None, schema_version=None):
     """
     Helper function with main, shared logic for to_parquet/to_feather.
     """
@@ -233,7 +235,7 @@ def _geopandas_to_arrow(df, index=None, version=None):
     _validate_dataframe(df)
 
     # create geo metadata before altering incoming data frame
-    geo_metadata = _create_metadata(df, version=version)
+    geo_metadata = _create_metadata(df, schema_version=schema_version)
 
     df = df.to_wkb()
 
@@ -243,10 +245,13 @@ def _geopandas_to_arrow(df, index=None, version=None):
     # This must be done AFTER creating the table or it is not persisted
     metadata = table.schema.metadata
     metadata.update({b"geo": _encode_metadata(geo_metadata)})
+
     return table.replace_schema_metadata(metadata)
 
 
-def _to_parquet(df, path, index=None, compression="snappy", version=None, **kwargs):
+def _to_parquet(
+    df, path, index=None, compression="snappy", schema_version=None, **kwargs
+):
     """
     Write a GeoDataFrame to the Parquet format.
 
@@ -270,7 +275,7 @@ def _to_parquet(df, path, index=None, compression="snappy", version=None, **kwar
         output except `RangeIndex` which is stored as metadata only.
     compression : {'snappy', 'gzip', 'brotli', None}, default 'snappy'
         Name of the compression to use. Use ``None`` for no compression.
-    version : {'0.1.0', '0.4.0', None}
+    schema_version : {'0.1.0', '0.4.0', None}
         GeoParquet specification version; if not provided will default to
         latest supported version.
     kwargs
@@ -280,12 +285,23 @@ def _to_parquet(df, path, index=None, compression="snappy", version=None, **kwar
         "pyarrow.parquet", extra="pyarrow is required for Parquet support."
     )
 
+    if kwargs and "version" in kwargs and kwargs["version"] is not None:
+        if schema_version is None and kwargs["version"] in SUPPORTED_VERSIONS:
+            warnings.warn(
+                "the `version` parameter has been replaced with `schema_version`. "
+                "`version` will instead be passed directly to the underlying "
+                "parquet writer unless `version` is 0.1.0 or 0.4.0.",
+                FutureWarning,
+                stacklevel=2,
+            )
+            schema_version = kwargs.pop("version")
+
     path = _expand_user(path)
-    table = _geopandas_to_arrow(df, index=index, version=version)
+    table = _geopandas_to_arrow(df, index=index, schema_version=schema_version)
     parquet.write_table(table, path, compression=compression, **kwargs)
 
 
-def _to_feather(df, path, index=None, compression=None, version=None, **kwargs):
+def _to_feather(df, path, index=None, compression=None, schema_version=None, **kwargs):
     """
     Write a GeoDataFrame to the Feather format.
 
@@ -310,7 +326,7 @@ def _to_feather(df, path, index=None, compression=None, version=None, **kwargs):
     compression : {'zstd', 'lz4', 'uncompressed'}, optional
         Name of the compression to use. Use ``"uncompressed"`` for no
         compression. By default uses LZ4 if available, otherwise uncompressed.
-    version : {'0.1.0', '0.4.0', None}
+    schema_version : {'0.1.0', '0.4.0', None}
         GeoParquet specification version; if not provided will default to
         latest supported version.
     kwargs
@@ -325,8 +341,19 @@ def _to_feather(df, path, index=None, compression=None, version=None, **kwargs):
     if Version(pyarrow.__version__) < Version("0.17.0"):
         raise ImportError("pyarrow >= 0.17 required for Feather support")
 
+    if kwargs and "version" in kwargs and kwargs["version"] is not None:
+        if schema_version is None and kwargs["version"] in SUPPORTED_VERSIONS:
+            warnings.warn(
+                "the `version` parameter has been replaced with `schema_version`. "
+                "`version` will instead be passed directly to the underlying "
+                "feather writer unless `version` is 0.1.0 or 0.4.0.",
+                FutureWarning,
+                stacklevel=2,
+            )
+            schema_version = kwargs.pop("version")
+
     path = _expand_user(path)
-    table = _geopandas_to_arrow(df, index=index, version=version)
+    table = _geopandas_to_arrow(df, index=index, schema_version=schema_version)
     feather.write_feather(table, path, compression=compression, **kwargs)
 
 
@@ -337,6 +364,7 @@ def _arrow_to_geopandas(table, metadata=None):
     df = table.to_pandas()
 
     metadata = metadata or table.schema.metadata
+
     if metadata is None or b"geo" not in metadata:
         raise ValueError(
             """Missing geo metadata in Parquet/Feather file.
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 8c1783fc..8c62af5e 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -341,9 +341,16 @@ 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], errors="ignore")
+                # if to_datetime failed, try again for mixed timezone offsets
+                if as_dt.dtype == "object":
+                    # This can still fail if there are invalid datetimes
+                    as_dt = pd.to_datetime(df[k], errors="ignore", utc=True)
+                # if to_datetime succeeded, round datetimes as
+                # fiona only supports up to ms precision (any microseconds are
+                # floating point rounding error)
+                if not (as_dt.dtype == "object"):
+                    df[k] = as_dt.dt.round(freq="ms")
             return df
 
 
diff --git a/geopandas/tools/_show_versions.py b/geopandas/tools/_show_versions.py
index 4798515f..3d03ca46 100644
--- a/geopandas/tools/_show_versions.py
+++ b/geopandas/tools/_show_versions.py
@@ -86,19 +86,22 @@ def _get_deps_info():
     """
     deps = [
         "geopandas",
-        "pandas",
-        "fiona",
+        # required deps
         "numpy",
-        "shapely",
-        "rtree",
+        "pandas",
         "pyproj",
+        "shapely",
+        # optional deps
+        "fiona",
+        "geoalchemy2",
+        "geopy",
         "matplotlib",
         "mapclassify",
-        "geopy",
+        "pygeos",
+        "pyogrio",
         "psycopg2",
-        "geoalchemy2",
         "pyarrow",
-        "pygeos",
+        "rtree",
     ]
 
     def get_version(module):


Test patch:
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 62657b99..ffe889aa 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -105,8 +105,8 @@ def test_crs_metadata_datum_ensemble():
 
 def test_write_metadata_invalid_spec_version():
     gdf = geopandas.GeoDataFrame(geometry=[box(0, 0, 10, 10)], crs="EPSG:4326")
-    with pytest.raises(ValueError, match="version must be one of"):
-        _create_metadata(gdf, version="invalid")
+    with pytest.raises(ValueError, match="schema_version must be one of"):
+        _create_metadata(gdf, schema_version="invalid")
 
 
 def test_encode_metadata():
@@ -259,7 +259,7 @@ def test_to_parquet_does_not_pass_engine_along(mock_to_parquet):
     # assert that engine keyword is not passed through to _to_parquet (and thus
     # parquet.write_table)
     mock_to_parquet.assert_called_with(
-        df, "", compression="snappy", index=None, version=None
+        df, "", compression="snappy", index=None, schema_version=None
     )
 
 
@@ -679,9 +679,10 @@ def test_write_read_default_crs(tmpdir, format):
 
 
 @pytest.mark.parametrize(
-    "format,version", product(["feather", "parquet"], [None] + SUPPORTED_VERSIONS)
+    "format,schema_version",
+    product(["feather", "parquet"], [None] + SUPPORTED_VERSIONS),
 )
-def test_write_spec_version(tmpdir, format, version):
+def test_write_spec_version(tmpdir, format, schema_version):
     if format == "feather":
         from pyarrow.feather import read_table
 
@@ -691,7 +692,7 @@ def test_write_spec_version(tmpdir, format, version):
     filename = os.path.join(str(tmpdir), f"test.{format}")
     gdf = geopandas.GeoDataFrame(geometry=[box(0, 0, 10, 10)], crs="EPSG:4326")
     write = getattr(gdf, f"to_{format}")
-    write(filename, version=version)
+    write(filename, schema_version=schema_version)
 
     # ensure that we can roundtrip data regardless of version
     read = getattr(geopandas, f"read_{format}")
@@ -700,10 +701,10 @@ def test_write_spec_version(tmpdir, format, version):
 
     table = read_table(filename)
     metadata = json.loads(table.schema.metadata[b"geo"])
-    assert metadata["version"] == version or METADATA_VERSION
+    assert metadata["version"] == schema_version or METADATA_VERSION
 
     # verify that CRS is correctly handled between versions
-    if version == "0.1.0":
+    if schema_version == "0.1.0":
         assert metadata["columns"]["geometry"]["crs"] == gdf.crs.to_wkt()
 
     else:
@@ -712,6 +713,46 @@ def test_write_spec_version(tmpdir, format, version):
         assert metadata["columns"]["geometry"]["crs"] == crs_expected
 
 
+@pytest.mark.parametrize(
+    "format,version", product(["feather", "parquet"], [None] + SUPPORTED_VERSIONS)
+)
+def test_write_deprecated_version_parameter(tmpdir, format, version):
+    if format == "feather":
+        from pyarrow.feather import read_table
+
+        version = version or 2
+
+    else:
+        from pyarrow.parquet import read_table
+
+        version = version or "2.6"
+
+    filename = os.path.join(str(tmpdir), f"test.{format}")
+    gdf = geopandas.GeoDataFrame(geometry=[box(0, 0, 10, 10)], crs="EPSG:4326")
+    write = getattr(gdf, f"to_{format}")
+
+    if version in SUPPORTED_VERSIONS:
+        with pytest.warns(
+            FutureWarning,
+            match="the `version` parameter has been replaced with `schema_version`",
+        ):
+            write(filename, version=version)
+
+    else:
+        # no warning raised if not one of the captured versions
+        write(filename, version=version)
+
+    table = read_table(filename)
+    metadata = json.loads(table.schema.metadata[b"geo"])
+
+    if version in SUPPORTED_VERSIONS:
+        # version is captured as a parameter
+        assert metadata["version"] == version
+    else:
+        # version is passed to underlying writer
+        assert metadata["version"] == METADATA_VERSION
+
+
 @pytest.mark.parametrize("version", ["0.1.0", "0.4.0"])
 def test_read_versioned_file(version):
     """
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index fcb9e54b..2ccbd949 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
 
@@ -32,12 +33,14 @@ except ImportError:
 try:
     import fiona
 
-    FIONA_GE_1814 = Version(fiona.__version__) >= Version(
-        "1.8.14"
-    )  # datetime roundtrip
+    # datetime roundtrip
+    FIONA_GE_1814 = Version(fiona.__version__) >= Version("1.8.14")
+    # invalid datetime handling
+    FIONA_GE_1821 = Version(fiona.__version__) >= Version("1.8.21")
 except ImportError:
     fiona = False
     FIONA_GE_1814 = False
+    FIONA_GE_1821 = False
 
 
 PYOGRIO_MARK = pytest.mark.skipif(not pyogrio, reason="pyogrio not installed")
@@ -231,6 +234,85 @@ def test_to_file_datetime(tmpdir, driver, ext, time, engine):
         assert_series_equal(df["b"], df_read["b"])
 
 
+dt_exts = ["gpkg", "geojson"]
+
+
+def write_invalid_date_file(date_str, tmpdir, ext, engine):
+    tempfilename = os.path.join(str(tmpdir), f"test_invalid_datetime.{ext}")
+    df = GeoDataFrame(
+        {
+            "date": ["2014-08-26T10:01:23", "2014-08-26T10:01:23", date_str],
+            "geometry": [Point(1, 1), Point(1, 1), Point(1, 1)],
+        }
+    )
+    # Schema not required for GeoJSON since not typed, but needed for GPKG
+    if ext == "geojson":
+        df.to_file(tempfilename)
+    else:
+        schema = {"geometry": "Point", "properties": {"date": "datetime"}}
+        if engine == "pyogrio" and not fiona:
+            # (use schema to write the invalid date without pandas datetimes
+            pytest.skip("test requires fiona kwarg schema")
+        df.to_file(tempfilename, schema=schema, engine="fiona")
+    return tempfilename
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_invalid(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if not FIONA_GE_1821 and ext == "gpkg":
+        # https://github.com/Toblerity/Fiona/issues/1035
+        pytest.skip("Invalid datetime throws in Fiona<1.8.21")
+
+    date_str = "9999-99-99T00:00:00"  # invalid date handled by GDAL
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    if ext == "gpkg":
+        assert is_datetime64_any_dtype(res["date"])
+        assert pd.isna(res["date"].iloc[-1])
+    else:
+        assert res["date"].dtype == "object"
+        assert isinstance(res["date"].iloc[-1], str)
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_out_of_bounds_ns(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if ext == "geojson":
+        skip_pyogrio_not_supported(engine)
+
+    date_str = "9999-12-31T00:00:00"  # valid to GDAL, not to [ns] format
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    # Pandas invalid datetimes are read in as object dtype (strings)
+    assert res["date"].dtype == "object"
+    assert isinstance(res["date"].iloc[0], str)
+
+
+def test_read_file_datetime_mixed_offsets(tmpdir):
+    # https://github.com/geopandas/geopandas/issues/2478
+    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)
+    # check mixed tz don't crash GH2478
+    res = read_file(tempfilename)
+    if FIONA_GE_1814:
+        # Convert mixed timezones to UTC equivalent
+        assert is_datetime64_any_dtype(res["date"])
+        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_geodataframe.py b/geopandas/tests/test_geodataframe.py
index a1a02ae2..3cff5104 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1220,6 +1220,25 @@ class TestConstructor:
         with pytest.raises(ValueError):
             GeoDataFrame(df3, geometry="geom")
 
+    @pytest.mark.parametrize("dtype", ["geometry", "object"])
+    def test_multiindex_with_geometry_label(self, dtype):
+        # DataFrame with MultiIndex where "geometry" label corresponds to
+        # multiple columns
+        df = pd.DataFrame([[Point(0, 0), Point(1, 1)], [Point(2, 2), Point(3, 3)]])
+        df = df.astype(dtype)
+        df.columns = pd.MultiIndex.from_product([["geometry"], [0, 1]])
+        # don't error in constructor
+        gdf = GeoDataFrame(df)
+        # Getting the .geometry column gives GeoDataFrame for both columns
+        # (but with first MultiIndex level removed)
+        # TODO should this give an error instead?
+        result = gdf.geometry
+        assert result.shape == gdf.shape
+        assert result.columns.tolist() == [0, 1]
+        assert_frame_equal(result, gdf["geometry"])
+        result = gdf[["geometry"]]
+        assert_frame_equal(result, gdf if dtype == "geometry" else pd.DataFrame(gdf))
+
 
 def test_geodataframe_crs():
     gdf = GeoDataFrame(columns=["geometry"])
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_op_output_types.py b/geopandas/tests/test_op_output_types.py
index 3a4155c7..5a447f39 100644
--- a/geopandas/tests/test_op_output_types.py
+++ b/geopandas/tests/test_op_output_types.py
@@ -289,6 +289,11 @@ def test_expanddim_in_unstack():
     else:  # pandas GH37369, unstack doesn't call finalize
         assert unstack._geometry_column_name == "geometry"
 
+    # https://github.com/geopandas/geopandas/issues/2486
+    s.name = "geometry"
+    unstack = s.unstack()
+    assert_object(unstack, GeoDataFrame, None, None)
+
 
 # indexing /  constructor_sliced tests
 
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 252af1d1..552d53bd 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -655,6 +655,22 @@ 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):
+    # https://github.com/geopandas/geopandas/issues/2057
+    # pivot failing due to creating a MultiIndex
+    result = df.pivot(columns="value1")
+    expected = GeoDataFrame(pd.DataFrame(df).pivot(columns="value1"))
+    # TODO assert_geodataframe_equal crashes
+    assert isinstance(result, GeoDataFrame)
+    assert_frame_equal(result, expected)
 
 
 def test_preserve_attrs(df):