Repository: geopandas/geopandas
Pull request #2456 (2022-06-09 03:28:38)

CI: add fiona latest to test runs
https://github.com/Toblerity/Fiona/issues/1053

I am checking to see if the CRS changes break anything.

martinfleis: Looking good!
snowman2: Logs say `fiona==1.9a1` and the CRS updates are in `1.9a2`. Must not have released them to pypi yet.
snowman2: Do you think this would be a good addition for geopandas? If so, would you prefer installing from pip or git?
martinfleis: I would prefer installing from wheels so we don't have to compile here.
snowman2: > I would prefer installing from wheels so we don't have to compile here.

That makes sense for the CI build. One downside is that it won't work on Windows for local development. Is that okay?
snowman2: Actually, I just realized that conda-forge has some patches it applies for Windows, so the experience will be the same either way.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c252afd0..2fd3f56a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,41 @@
 Changelog
 =========
 
+Development version
+-------------------
+
+New features and improvements:
+
+Deprecations and compatibility notes:
+
+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)
 ----------------------------
 
@@ -121,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)
 ---------------------------------
 
@@ -141,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)
 --------------------------------
 
@@ -150,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)
 --------------------------------
 
@@ -187,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:
 
@@ -237,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)
 ---------------------------------
 
@@ -354,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)
 -----------------------------
 
@@ -372,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)
 -----------------------------
 
@@ -471,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)
 ---------------------------------
 
@@ -516,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)
 ---------------------------------
 
@@ -527,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)
 ---------------------------------
 
@@ -540,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)
 --------------------------------
 
@@ -549,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)
 ----------------------------------
 
@@ -590,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/CONTRIBUTING.md b/CONTRIBUTING.md
index 76bbf2fb..9a2562a0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,6 +32,9 @@ In particular, when submitting a pull request:
   line of a docstring should be a standalone summary. Parameters and
   return values should be documented explicitly.
 
+- Unless your PR implements minor changes or internal work only, make sure
+  it contains a note describing the changes in the `CHANGELOG.md` file.
+
 Improving the documentation and testing for code already in GeoPandas
 is a great way to get started if you'd like to make a contribution.
 
diff --git a/ci/envs/310-dev.yaml b/ci/envs/310-dev.yaml
index e1d5e7ed..0c250d6b 100644
--- a/ci/envs/310-dev.yaml
+++ b/ci/envs/310-dev.yaml
@@ -6,7 +6,6 @@ dependencies:
   - cython
   # required
   - shapely
-  - fiona
   - pyproj
   - geos
   - packaging
@@ -28,6 +27,7 @@ dependencies:
     # dev versions of packages
     - --pre --extra-index https://pypi.anaconda.org/scipy-wheels-nightly/simple
     - numpy
+    - fiona
     - git+https://github.com/pandas-dev/pandas.git@main
     - git+https://github.com/matplotlib/matplotlib.git@main
     # - git+https://github.com/Toblerity/Shapely.git@main
diff --git a/doc/source/community/contributing.rst b/doc/source/community/contributing.rst
index 1eb9520e..5af97121 100644
--- a/doc/source/community/contributing.rst
+++ b/doc/source/community/contributing.rst
@@ -47,6 +47,9 @@ In particular, when submitting a pull request:
 - GeoPandas supports Python 3.8+ only. The last version of GeoPandas
   supporting Python 2 is 0.6.
 
+- Unless your PR implements minor changes or internal work only, make sure
+  it contains a note describing the changes in the `CHANGELOG.md` file.
+
 
 Seven Steps for Contributing
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/source/docs/user_guide/aggregation_with_dissolve.rst b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
index c8a10cd3..5c86aa2c 100644
--- a/doc/source/docs/user_guide/aggregation_with_dissolve.rst
+++ b/doc/source/docs/user_guide/aggregation_with_dissolve.rst
@@ -80,7 +80,7 @@ However it also accepts other summary statistic options as allowed by :meth:`pan
 * list of functions and/or function names, e.g. [np.sum, 'mean']
 * dict of axis labels -> functions, function names or list of such.
 
-For example, to get the number of contries on each continent, 
+For example, to get the number of countries on each continent, 
 as well as the populations of the largest and smallest country of each,
 we can aggregate the ``'name'`` column using ``'count'``,
 and the ``'pop_est'`` column using ``'min'`` and ``'max'``:
@@ -96,4 +96,4 @@ and the ``'pop_est'`` column using ``'min'`` and ``'max'``:
         },
     )
 
-   continents.head()
\ No newline at end of file
+   continents.head()
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):