Repository: geopandas/geopandas
Pull request #2624 (2022-10-29 14:02:49)

ENH: expose get_coordinates as a method
This exposes `get_coordinates` from shapely2/pygeos as a GeoSeries/GeoDataFrame method, returning a pandas.DataFrame. The index handling mirrors the implementation we have in `explode` as both are very similar operations in nature. One is returing parts of original geometries, the other goes one step further and returns coordinates.

I am aware that this is a borderline function we weren't sure we want to expose as a method but given there is a relatively close precedent with `explode` and we can reuse its indexing machinery, it is in the end a nice addition I think. 

I will add tests and documentation once there's an agreement that we want to do this and we have the API set.

Current behaviour:
```py
>>> from shapely.geometry import Point, LineString, Polygon
>>> s = geopandas.GeoSeries(
...     [
...         Point(1, 1),
...         LineString([(1, -1), (1, 0)]),
...         Polygon([(3, -1), (4, 0), (3, 1)]),
...     ]
... )
>>> s
0                              POINT (1.00000 1.00000)
1       LINESTRING (1.00000 -1.00000, 1.00000 0.00000)
2    POLYGON ((3.00000 -1.00000, 4.00000 0.00000, 3...
dtype: geometry
```
```py
>>> s.get_coordinates()
     x    y
0  1.0  1.0
1  1.0 -1.0
1  1.0  0.0
2  3.0 -1.0
2  4.0  0.0
2  3.0  1.0
2  3.0 -1.0
```
```py
>>> s.get_coordinates(ignore_index=True)
     x    y
0  1.0  1.0
1  1.0 -1.0
2  1.0  0.0
3  3.0 -1.0
4  4.0  0.0
5  3.0  1.0
6  3.0 -1.0
```
```py
>>> s.get_coordinates(index_parts=True)
       x    y
0 0  1.0  1.0
1 0  1.0 -1.0
  1  1.0  0.0
2 0  3.0 -1.0
  1  4.0  0.0
  2  3.0  1.0
  3  3.0 -1.0
```

martinfleis: This should be ready now.

@m-richards I've moved the exploded index handling into a helper function that is now shared by both explode and get_coordinates. Nice tip! 

> one might expect the gdf case to return a geodataframe reindexed to correspond to all the coordinates

not sure I agree. I would expect it to behave as `.bounds` does. Can limit it to the GeoSeries but not sure about that. Or you'd rather have it behave the same as explode on gdf?
m-richards: > not sure I agree. I would expect it to behave as `.bounds` does. Can limit it to the GeoSeries but not sure about that. Or you'd rather have it behave the same as explode on gdf?

I actually agree with you - I think the current behaviour makes the most sense in terms of how I might use this method, and the parallel with bounds is a good justification - just thought I'd mention it though so we're happy this is the expected behaviour.


martinfleis: @jorisvandenbossche can we try to get this in?

Patch:
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 00000000..932c2ca9
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,8 @@
+# black-ification of code
+479b6e706293ff1a484e4e121682f3a105a53aa9
+7bc3166cfecdd120b544701cff5e37b4482cbbb5
+f4b749d148f0eb13e7e92fbbf7e2eae79de2543a
+373ac8bf731db1002e09859587c475a5f56c4e8b
+50c59d395bc539eb411f9a5c6cdf04d37510060a
+5dc982edbfc2633cd2c7a22ebd314d1b9b810a9a
+44dba4fde90e8010ea6c71c6cf51b34413440177
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bb9a199b..2f204cae 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,7 +5,7 @@ ci:
 files: 'geopandas\/'
 repos:
     - repo: https://github.com/psf/black
-      rev: 22.3.0
+      rev: 23.1.0
       hooks:
           - id: black
             language_version: python3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fbfb46f..2950b2a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 New features and improvements:
 
+- Added ``get_coordinates()`` method from shapely to GeoSeries/GeoDataframe (#2624).
 - The Parquet and Feather IO functions now support the latest 1.0.0-beta.1 version
   of the GeoParquet specification (geoparquet.org) (#2663).
 - New ``hilbert_distance()`` method that calculates the distance along a Hilbert curve
@@ -24,6 +25,7 @@ Bug fixes:
 - Escape special characters to avoid TemplateSyntaxError in ``explore()`` (#2657)
 - Fix `to_parquet`/`to_feather` to not write an invalid bbox (with NaNs) in the
   metadata in case of an empty GeoDataFrame (#2653)
+- Fix `to_parquet`/`to_feather` to use correct WKB flavor for 3D geometries (#2654)
 
 Notes on (optional) dependencies:
 
@@ -32,7 +34,6 @@ Notes on (optional) dependencies:
   dependencies have now changed to shapely 1.7.1, fiona 1.8.19, pyproj 3.0.1 and
   matplotlib 3.3.4 (#2655)
 
-
 ## Version 0.12.2 (December 10, 2022)
 
 Bug fixes:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9a2562a0..ec895527 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,4 +69,8 @@ Style
 
   Then `black` and `flake8` will be run automatically each time you
   commit changes. You can skip these checks with `git commit
-  --no-verify`.
+  --no-verify`. You can also configure your local git clone to have
+  `git blame` ignore the commits that introduced large formatting-only
+  changes with:
+
+    $ git config blame.ignoreRevsFile .git-blame-ignore-revs
diff --git a/benchmarks/io.py b/benchmarks/io.py
index 70cf845c..a2a1eb1f 100644
--- a/benchmarks/io.py
+++ b/benchmarks/io.py
@@ -44,7 +44,6 @@ format_dict = {
 
 
 class Bench:
-
     params = ["ESRI Shapefile", "GeoJSON", "GPKG", "Parquet", "Feather"]
     param_names = ["file_format"]
 
@@ -74,7 +73,6 @@ class Bench:
 
 
 class BenchFrame(Bench):
-
     params = ["ESRI Shapefile", "GeoJSON", "GPKG", "Parquet", "Feather"]
     param_names = ["file_format"]
 
@@ -88,7 +86,6 @@ class BenchFrame(Bench):
 
 
 class BenchSeries(Bench):
-
     params = ["ESRI Shapefile", "GeoJSON", "GPKG"]
     param_names = ["file_format"]
 
diff --git a/benchmarks/overlay.py b/benchmarks/overlay.py
index a8aafa65..cd352e59 100644
--- a/benchmarks/overlay.py
+++ b/benchmarks/overlay.py
@@ -4,7 +4,6 @@ from shapely.geometry import Point, Polygon
 
 
 class Countries:
-
     param_names = ["how"]
     params = [
         ("intersection", "union", "identity", "symmetric_difference", "difference")
@@ -26,7 +25,6 @@ class Countries:
 
 
 class Small:
-
     param_names = ["how"]
     params = [
         ("intersection", "union", "identity", "symmetric_difference", "difference")
@@ -56,14 +54,12 @@ class Small:
 
 
 class ManyPoints:
-
     param_names = ["how"]
     params = [
         ("intersection", "union", "identity", "symmetric_difference", "difference")
     ]
 
     def setup(self, *args):
-
         points = GeoDataFrame(geometry=[Point(i, i) for i in range(1000)])
         base = np.array([[0, 0], [0, 100], [100, 100], [100, 0]])
         polys = GeoDataFrame(geometry=[Polygon(base + i * 100) for i in range(10)])
diff --git a/benchmarks/plotting.py b/benchmarks/plotting.py
index 5731bdbd..608b510d 100644
--- a/benchmarks/plotting.py
+++ b/benchmarks/plotting.py
@@ -6,12 +6,10 @@ import numpy as np
 
 
 class Bench:
-
     param_names = ["geom_type"]
     params = [("Point", "LineString", "Polygon", "MultiPolygon", "mixed")]
 
     def setup(self, geom_type):
-
         if geom_type == "Point":
             geoms = GeoSeries([Point(i, i) for i in range(1000)])
         elif geom_type == "LineString":
diff --git a/benchmarks/sindex.py b/benchmarks/sindex.py
index 24fac10e..0510f035 100644
--- a/benchmarks/sindex.py
+++ b/benchmarks/sindex.py
@@ -32,7 +32,6 @@ def generate_test_df():
 
 
 class BenchIntersection:
-
     param_names = ["input_geom_type", "tree_geom_type"]
     params = [
         geom_types,
@@ -54,7 +53,6 @@ class BenchIntersection:
 
 
 class BenchIndexCreation:
-
     param_names = ["tree_geom_type"]
     params = [
         geom_types,
@@ -81,7 +79,6 @@ class BenchIndexCreation:
 
 
 class BenchQuery:
-
     param_names = ["predicate", "input_geom_type", "tree_geom_type"]
     params = [
         predicates,
diff --git a/benchmarks/sjoin.py b/benchmarks/sjoin.py
index 6edc44b6..0519ea0c 100644
--- a/benchmarks/sjoin.py
+++ b/benchmarks/sjoin.py
@@ -6,7 +6,6 @@ import numpy as np
 
 
 class Bench:
-
     param_names = ["op"]
     params = [("intersects", "contains", "within")]
 
diff --git a/ci/envs/310-dev.yaml b/ci/envs/310-dev.yaml
index 8efed121..b6ae56fc 100644
--- a/ci/envs/310-dev.yaml
+++ b/ci/envs/310-dev.yaml
@@ -16,7 +16,7 @@ dependencies:
   # optional
   - rtree
   #- geopy
-  - SQLalchemy
+  - SQLalchemy<2
   - libspatialite
   - pyarrow
   - pip
diff --git a/ci/envs/39-pd13-conda-forge.yaml b/ci/envs/39-pd13-conda-forge.yaml
index cc038cee..f7ca98b2 100644
--- a/ci/envs/39-pd13-conda-forge.yaml
+++ b/ci/envs/39-pd13-conda-forge.yaml
@@ -26,7 +26,7 @@ dependencies:
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
-  - SQLalchemy
+  - SQLalchemy<2
   - psycopg2
   - libspatialite
   - geoalchemy2
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index 7d7317b9..db095a36 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -29,6 +29,7 @@ General methods and attributes
    GeoSeries.x
    GeoSeries.y
    GeoSeries.z
+   GeoSeries.get_coordinates
 
 Unary predicates
 ----------------
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index 1923c136..8bbc5a91 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -86,7 +86,6 @@ def set_use_pygeos(val=None):
         USE_PYGEOS = bool(val)
     else:
         if USE_PYGEOS is None:
-
             USE_PYGEOS = HAS_PYGEOS
 
             if env_use_pygeos is not None:
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index 36408804..19e51fa4 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -255,7 +255,6 @@ def _points_from_xy(x, y, z=None):
 
 
 def points_from_xy(x, y, z=None):
-
     x = np.asarray(x, dtype="float64")
     y = np.asarray(y, dtype="float64")
     if z is not None:
diff --git a/geopandas/base.py b/geopandas/base.py
index d72a7edc..4cd46384 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -8,6 +8,7 @@ from shapely.geometry import box
 from shapely.geometry.base import BaseGeometry
 
 from .array import GeometryArray, GeometryDtype
+from . import _compat as compat
 
 
 def is_geometry_type(data):
@@ -3484,6 +3485,111 @@ GeometryCollection
             return False
         return self._data.equals(other._data)
 
+    def get_coordinates(self, include_z=False, ignore_index=False, index_parts=False):
+        """Gets coordinates from a :class:`GeoSeries` as a :class:`~pandas.DataFrame` of
+        floats.
+
+        The shape of the returned :class:`~pandas.DataFrame` is (N, 2), with N being the
+        number of coordinate pairs. With the default of ``include_z=False``,
+        three-dimensional data is ignored. When specifying ``include_z=True``, the shape
+        of the returned :class:`~pandas.DataFrame` is (N, 3).
+
+        Parameters
+        ----------
+        include_z : bool, default False
+            Include Z coordinates
+        ignore_index : bool, default False
+            If True, the resulting index will be labelled 0, 1, …, n - 1, ignoring
+            ``index_parts``.
+        index_parts : bool, default False
+           If True, the resulting index will be a :class:`~pandas.MultiIndex` (original
+           index with an additional level indicating the ordering of the coordinate
+           pairs: a new zero-based index for each geometry in the original GeoSeries).
+
+        Returns
+        -------
+        pandas.DataFrame
+
+        Examples
+        --------
+        >>> from shapely.geometry import Point, LineString, Polygon
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Point(1, 1),
+        ...         LineString([(1, -1), (1, 0)]),
+        ...         Polygon([(3, -1), (4, 0), (3, 1)]),
+        ...     ]
+        ... )
+        >>> s
+        0                              POINT (1.00000 1.00000)
+        1       LINESTRING (1.00000 -1.00000, 1.00000 0.00000)
+        2    POLYGON ((3.00000 -1.00000, 4.00000 0.00000, 3...
+        dtype: geometry
+
+        >>> s.get_coordinates()
+             x    y
+        0  1.0  1.0
+        1  1.0 -1.0
+        1  1.0  0.0
+        2  3.0 -1.0
+        2  4.0  0.0
+        2  3.0  1.0
+        2  3.0 -1.0
+
+        >>> s.get_coordinates(ignore_index=True)
+             x    y
+        0  1.0  1.0
+        1  1.0 -1.0
+        2  1.0  0.0
+        3  3.0 -1.0
+        4  4.0  0.0
+        5  3.0  1.0
+        6  3.0 -1.0
+
+        >>> s.get_coordinates(index_parts=True)
+               x    y
+        0 0  1.0  1.0
+        1 0  1.0 -1.0
+          1  1.0  0.0
+        2 0  3.0 -1.0
+          1  4.0  0.0
+          2  3.0  1.0
+          3  3.0 -1.0
+        """
+        if compat.USE_SHAPELY_20:
+            import shapely
+
+            coords, outer_idx = shapely.get_coordinates(
+                self.geometry.values, include_z=include_z, return_index=True
+            )
+        elif compat.USE_PYGEOS:
+            import pygeos
+
+            coords, outer_idx = pygeos.get_coordinates(
+                self.geometry.values.data, include_z=include_z, return_index=True
+            )
+
+        else:
+            import shapely
+
+            raise NotImplementedError(
+                f"shapely >= 2.0 or PyGEOS are required, "
+                f"version {shapely.__version__} is installed."
+            )
+
+        column_names = ["x", "y"]
+        if include_z:
+            column_names.append("z")
+
+        index = _get_index_for_parts(
+            self.index,
+            outer_idx,
+            ignore_index=ignore_index,
+            index_parts=index_parts,
+        )
+
+        return pd.DataFrame(coords, index=index, columns=column_names)
+
     def hilbert_distance(self, total_bounds=None, level=16):
         """
         Calculate the distance along a Hilbert curve.
@@ -3517,6 +3623,61 @@ GeometryCollection
         return distances
 
 
+def _get_index_for_parts(orig_idx, outer_idx, ignore_index, index_parts):
+    """Helper to handle index when geometries get exploded to parts.
+
+    Used in get_coordinates and explode.
+
+    Parameters
+    ----------
+    orig_idx : pandas.Index
+        original index
+    outer_idx : array
+        the index of each returned geometry as a separate ndarray of integers
+    ignore_index : bool
+    index_parts : bool
+
+    Returns
+    -------
+    pandas.Index
+        index or multiindex
+    """
+
+    if ignore_index:
+        return None
+    else:
+        if len(outer_idx):
+            # Generate inner index as a range per value of outer_idx
+            # 1. identify the start of each run of values in outer_idx
+            # 2. count number of values per run
+            # 3. use cumulative sums to create an incremental range
+            #    starting at 0 in each run
+            run_start = np.r_[True, outer_idx[:-1] != outer_idx[1:]]
+            counts = np.diff(np.r_[np.nonzero(run_start)[0], len(outer_idx)])
+            inner_index = (~run_start).cumsum()
+            inner_index -= np.repeat(inner_index[run_start], counts)
+
+        else:
+            inner_index = []
+
+        # extract original index values based on integer index
+        outer_index = orig_idx.take(outer_idx)
+
+        if index_parts:
+            nlevels = outer_index.nlevels
+            index_arrays = [outer_index.get_level_values(lvl) for lvl in range(nlevels)]
+            index_arrays.append(inner_index)
+
+            index = pd.MultiIndex.from_arrays(
+                index_arrays, names=orig_idx.names + [None]
+            )
+
+        else:
+            index = outer_index
+
+    return index
+
+
 class _CoordinateIndexer(object):
     # see docstring GeoPandasBase.cx property above
 
diff --git a/geopandas/explore.py b/geopandas/explore.py
index 21b93d06..d2b92202 100644
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -418,7 +418,6 @@ GON (((180.00000 -16.06713, 180.00000...
 
             # colormap exists in matplotlib
             if cmap in plt.colormaps():
-
                 color = np.apply_along_axis(
                     colors.to_hex,
                     1,
@@ -456,7 +455,6 @@ GON (((180.00000 -16.06713, 180.00000...
 
             # get bins
             if scheme is not None:
-
                 if classification_kwds is None:
                     classification_kwds = {}
                 if "k" not in classification_kwds:
@@ -472,7 +470,6 @@ GON (((180.00000 -16.06713, 180.00000...
                 )
 
             else:
-
                 bins = np.linspace(vmin, vmax, 257)[1:]
                 binning = classify(
                     np.asarray(gdf[column][~nan_idx]), "UserDefined", bins=bins
@@ -656,7 +653,6 @@ GON (((180.00000 -16.06713, 180.00000...
 
             _categorical_legend(m, caption, categories, legend_colors)
         elif column is not None:
-
             cbar = legend_kwds.pop("colorbar", True)
             colormap_kwds = {}
             if "max_labels" in legend_kwds:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 1c885628..2e543885 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -910,7 +910,6 @@ individually so that features may have different properties
 
         else:
             for fid, geom in zip(ids, geometries):
-
                 if drop_id:
                     feature = {}
                 else:
@@ -952,6 +951,7 @@ individually so that features may have different properties
             The default is to return a binary bytes object.
         kwargs
             Additional keyword args will be passed to
+            :func:`shapely.to_wkb` if shapely >= 2 is installed or
             :func:`pygeos.to_wkb` if pygeos is installed.
 
         Returns
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 4256d828..3ff6fff5 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -860,6 +860,8 @@ class GeoSeries(GeoPandasBase, Series):
         GeoDataFrame.explode
 
         """
+        from .base import _get_index_for_parts
+
         if index_parts is None and not ignore_index:
             warnings.warn(
                 "Currently, index_parts defaults to True, but in the future, "
@@ -883,38 +885,12 @@ class GeoSeries(GeoPandasBase, Series):
                     self.values.data, return_index=True
                 )
 
-            if len(outer_idx):
-                # Generate inner index as a range per value of outer_idx
-                # 1. identify the start of each run of values in outer_idx
-                # 2. count number of values per run
-                # 3. use cumulative sums to create an incremental range
-                #    starting at 0 in each run
-                run_start = np.r_[True, outer_idx[:-1] != outer_idx[1:]]
-                counts = np.diff(np.r_[np.nonzero(run_start)[0], len(outer_idx)])
-                inner_index = (~run_start).cumsum()
-                inner_index -= np.repeat(inner_index[run_start], counts)
-
-            else:
-                inner_index = []
-
-            # extract original index values based on integer index
-            outer_index = self.index.take(outer_idx)
-            if ignore_index:
-                index = range(len(geometries))
-
-            elif index_parts:
-                nlevels = outer_index.nlevels
-                index_arrays = [
-                    outer_index.get_level_values(lvl) for lvl in range(nlevels)
-                ]
-                index_arrays.append(inner_index)
-
-                index = MultiIndex.from_arrays(
-                    index_arrays, names=self.index.names + [None]
-                )
-
-            else:
-                index = outer_index
+            index = _get_index_for_parts(
+                self.index,
+                outer_idx,
+                ignore_index=ignore_index,
+                index_parts=index_parts,
+            )
 
             return GeoSeries(geometries, index=index, crs=self.crs).__finalize__(self)
 
@@ -1209,6 +1185,7 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
             The default is to return a binary bytes object.
         kwargs
             Additional keyword args will be passed to
+            :func:`shapely.to_wkb` if shapely >= 2 is installed or
             :func:`pygeos.to_wkb` if pygeos is installed.
 
         Returns
diff --git a/geopandas/io/arrow.py b/geopandas/io/arrow.py
index f9dcbbca..4ab2d816 100644
--- a/geopandas/io/arrow.py
+++ b/geopandas/io/arrow.py
@@ -5,6 +5,7 @@ import warnings
 import numpy as np
 from pandas import DataFrame, Series
 
+import geopandas._compat as compat
 from geopandas._compat import import_optional_dependency
 from geopandas.array import from_wkb
 from geopandas import GeoDataFrame
@@ -246,7 +247,23 @@ def _geopandas_to_arrow(df, index=None, schema_version=None):
     # create geo metadata before altering incoming data frame
     geo_metadata = _create_metadata(df, schema_version=schema_version)
 
-    df = df.to_wkb()
+    kwargs = {}
+    if compat.USE_SHAPELY_20:
+        kwargs = dict(flavor="iso")
+    else:
+        for col in df.columns[df.dtypes == "geometry"]:
+            series = df[col]
+            if series.has_z.any():
+                warnings.warn(
+                    "The GeoDataFrame contains 3D geometries, and when using "
+                    "shapely < 2.0, such geometries will be written not exactly "
+                    "following to the GeoParquet spec (not using ISO WKB). For "
+                    "most use cases this should not be a problem (GeoPandas can "
+                    "read such files fine).",
+                    stacklevel=2,
+                )
+                break
+    df = df.to_wkb(**kwargs)
 
     table = Table.from_pandas(df, preserve_index=index)
 
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 3cfb795c..a8da0a21 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -550,7 +550,6 @@ def _to_file(
 
 
 def _to_file_fiona(df, filename, driver, schema, crs, mode, **kwargs):
-
     if schema is None:
         schema = infer_schema(df)
 
diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py
index 1e8db4da..2c25ff86 100644
--- a/geopandas/io/sql.py
+++ b/geopandas/io/sql.py
@@ -441,7 +441,6 @@ def _write_postgis(
                     raise ValueError(msg)
 
     with _get_conn(con) as connection:
-
         gdf.to_sql(
             name,
             connection,
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 91a9f49d..bd2fcb32 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -896,7 +896,6 @@ GON (((-122.84000 49.00000, -120.0000...
         plot_series(expl_series[nan_idx], ax=ax, **merged_kwds)
 
     if legend and not color:
-
         if legend_kwds is None:
             legend_kwds = {}
         if "fmt" in legend_kwds:
@@ -950,7 +949,6 @@ GON (((-122.84000 49.00000, -120.0000...
             legend_kwds.setdefault("loc", "best")
             ax.legend(patches, categories, **legend_kwds)
         else:
-
             if cax is not None:
                 legend_kwds.setdefault("cax", cax)
             else:
@@ -965,7 +963,6 @@ GON (((-122.84000 49.00000, -120.0000...
 
 @doc(plot_dataframe)
 class GeoplotAccessor(PlotAccessor):
-
     _pandas_kinds = PlotAccessor._all_kinds
 
     def __call__(self, *args, **kwargs):
diff --git a/geopandas/sindex.py b/geopandas/sindex.py
index b37755bf..7950e000 100644
--- a/geopandas/sindex.py
+++ b/geopandas/sindex.py
@@ -357,7 +357,6 @@ geometries}
 
 
 if compat.HAS_RTREE:
-
     import rtree.index  # noqa
     from rtree.core import RTreeError  # noqa
     from shapely.prepared import prep  # noqa
@@ -628,7 +627,6 @@ if compat.HAS_RTREE:
 
 
 if compat.SHAPELY_GE_20 or compat.HAS_PYGEOS:
-
     from . import geoseries  # noqa
     from . import array  # noqa
 
diff --git a/geopandas/tools/geocoding.py b/geopandas/tools/geocoding.py
index bba8bb65..5ea5376f 100644
--- a/geopandas/tools/geocoding.py
+++ b/geopandas/tools/geocoding.py
@@ -162,7 +162,6 @@ def _prepare_geocode_result(results):
     index = []
 
     for i, s in results.items():
-
         if s is None:
             p = Point()
             address = None
diff --git a/geopandas/tools/hilbert_curve.py b/geopandas/tools/hilbert_curve.py
index 364b0ef4..42189a41 100644
--- a/geopandas/tools/hilbert_curve.py
+++ b/geopandas/tools/hilbert_curve.py
@@ -126,7 +126,6 @@ def _interleave(x):
 
 
 def _encode(level, x, y):
-
     x = np.asarray(x, dtype="uint32")
     y = np.asarray(y, dtype="uint32")
 


Test patch:
diff --git a/ci/envs/310-latest-conda-forge.yaml b/ci/envs/310-latest-conda-forge.yaml
index 0b483471..d44e764f 100644
--- a/ci/envs/310-latest-conda-forge.yaml
+++ b/ci/envs/310-latest-conda-forge.yaml
@@ -25,7 +25,7 @@ dependencies:
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
-  - SQLalchemy
+  - SQLalchemy<2
   - psycopg2
   - libspatialite
   - geoalchemy2
diff --git a/ci/envs/311-latest-conda-forge.yaml b/ci/envs/311-latest-conda-forge.yaml
index 6b1b73c6..dd3e0a95 100644
--- a/ci/envs/311-latest-conda-forge.yaml
+++ b/ci/envs/311-latest-conda-forge.yaml
@@ -26,7 +26,7 @@ dependencies:
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
-  - SQLalchemy
+  - SQLalchemy<2
   - psycopg2
   - libspatialite
   - geoalchemy2
diff --git a/ci/envs/39-latest-conda-forge.yaml b/ci/envs/39-latest-conda-forge.yaml
index d39c54d4..75732e35 100644
--- a/ci/envs/39-latest-conda-forge.yaml
+++ b/ci/envs/39-latest-conda-forge.yaml
@@ -28,7 +28,7 @@ dependencies:
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
-  - SQLalchemy
+  - SQLalchemy<2
   - psycopg2
   - libspatialite
   - geoalchemy2
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 53933df2..030ffbca 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -15,6 +15,7 @@ from shapely.geometry import box, Point, MultiPolygon
 
 
 import geopandas
+import geopandas._compat as compat
 from geopandas import GeoDataFrame, read_file, read_parquet, read_feather
 from geopandas.array import to_wkb
 from geopandas.datasets import get_path
@@ -692,6 +693,28 @@ def test_write_read_default_crs(tmpdir, format):
     assert df.crs.equals(pyproj.CRS("OGC:CRS84"))
 
 
+def test_write_iso_wkb(tmpdir):
+    gdf = geopandas.GeoDataFrame(
+        geometry=geopandas.GeoSeries.from_wkt(["POINT Z (1 2 3)"])
+    )
+    if compat.USE_SHAPELY_20:
+        gdf.to_parquet(tmpdir / "test.parquet")
+    else:
+        with pytest.warns(UserWarning, match="The GeoDataFrame contains 3D geometries"):
+            gdf.to_parquet(tmpdir / "test.parquet")
+
+    from pyarrow.parquet import read_table
+
+    table = read_table(tmpdir / "test.parquet")
+    wkb = table["geometry"][0].as_py().hex()
+
+    if compat.USE_SHAPELY_20:
+        # correct ISO flavor
+        assert wkb == "01e9030000000000000000f03f00000000000000400000000000000840"
+    else:
+        assert wkb == "0101000080000000000000f03f00000000000000400000000000000840"
+
+
 @pytest.mark.parametrize(
     "format,schema_version",
     product(["feather", "parquet"], [None] + SUPPORTED_VERSIONS),
@@ -699,7 +722,6 @@ def test_write_read_default_crs(tmpdir, format):
 def test_write_spec_version(tmpdir, format, schema_version):
     if format == "feather":
         from pyarrow.feather import read_table
-
     else:
         from pyarrow.parquet import read_table
 
diff --git a/geopandas/tests/test_dissolve.py b/geopandas/tests/test_dissolve.py
index 8192a0c8..23808826 100644
--- a/geopandas/tests/test_dissolve.py
+++ b/geopandas/tests/test_dissolve.py
@@ -334,7 +334,6 @@ def test_dissolve_dropna_warn(nybb_polydf):
 
 
 def test_dissolve_multi_agg(nybb_polydf, merged_shapes):
-
     merged_shapes[("BoroCode", "min")] = [3, 1]
     merged_shapes[("BoroCode", "max")] = [5, 2]
     merged_shapes[("BoroName", "count")] = [3, 2]
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 3e7c05d9..c9cc8a5f 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -843,7 +843,6 @@ class TestExplore:
         assert '{"color":"red","fillOpacity":1}' in out_str
 
     def test_custom_colormaps(self):
-
         step = StepColormap(["green", "yellow", "red"], vmin=0, vmax=100000000)
 
         m = self.world.explore("pop_est", cmap=step, tooltip=["name"], legend=True)
diff --git a/geopandas/tests/test_extension_array.py b/geopandas/tests/test_extension_array.py
index 9582404b..30a5a932 100644
--- a/geopandas/tests/test_extension_array.py
+++ b/geopandas/tests/test_extension_array.py
@@ -288,7 +288,6 @@ def all_compare_operators(request):
 
 
 class TestDtype(extension_tests.BaseDtypeTests):
-
     # additional tests
 
     def test_array_type_with_arg(self, data, dtype):
diff --git a/geopandas/tests/test_geocode.py b/geopandas/tests/test_geocode.py
index 4b1c8ccb..ae68d7d7 100644
--- a/geopandas/tests/test_geocode.py
+++ b/geopandas/tests/test_geocode.py
@@ -111,7 +111,6 @@ def test_prepare_result_none():
 
 @pytest.mark.parametrize("geocode_result", (None, (None, None)))
 def test_prepare_geocode_result_when_result_is(geocode_result):
-
     result = {0: geocode_result}
     expected_output = GeoDataFrame(
         {"geometry": [Point()], "address": [None]},
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 6efbbd23..632db49e 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1033,7 +1033,6 @@ class TestConstructor:
         assert_series_equal(df["A"], pd.Series(range(3), name="A"))
 
     def test_dict_of_series(self):
-
         data = {
             "A": pd.Series(range(3)),
             "B": pd.Series(np.arange(3.0)),
@@ -1058,7 +1057,6 @@ class TestConstructor:
             GeoDataFrame(data, index=[1, 2])
 
     def test_dict_specified_geometry(self):
-
         data = {
             "A": range(3),
             "B": np.arange(3.0),
@@ -1120,7 +1118,6 @@ class TestConstructor:
         assert type(pddf) == pd.DataFrame
 
         for df in [gpdf, pddf]:
-
             res = GeoDataFrame(df)
             check_geodataframe(res)
 
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 7a093529..bacc908d 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -122,6 +122,53 @@ class TestGeomMethods:
             {"geometry": self.gz, "col3": [4, 5, 6], "col4": ["rand", "string", "geo"]}
         )
 
+        self.g11 = GeoSeries(
+            [
+                self.p0,
+                self.p3d,
+                self.pt_empty,
+                self.t1,
+                self.tz,
+                self.empty_poly,
+                self.l1,
+            ]
+        )
+        # expected coordinates from g11
+        self.expected_2d = np.array(
+            [
+                [5.0, 5.0],
+                [5.0, 5.0],
+                [0.0, 0.0],
+                [1.0, 0.0],
+                [1.0, 1.0],
+                [0.0, 0.0],
+                [1.0, 1.0],
+                [2.0, 2.0],
+                [3.0, 3.0],
+                [1.0, 1.0],
+                [0.0, 0.0],
+                [0.0, 1.0],
+                [1.0, 1.0],
+            ]
+        )
+        self.expected_3d = np.array(
+            [
+                [5.0, 5.0, np.nan],
+                [5.0, 5.0, 5.0],
+                [0.0, 0.0, np.nan],
+                [1.0, 0.0, np.nan],
+                [1.0, 1.0, np.nan],
+                [0.0, 0.0, np.nan],
+                [1.0, 1.0, 1.0],
+                [2.0, 2.0, 2.0],
+                [3.0, 3.0, 3.0],
+                [1.0, 1.0, 1.0],
+                [0.0, 0.0, np.nan],
+                [0.0, 1.0, np.nan],
+                [1.0, 1.0, np.nan],
+            ]
+        )
+
     def _test_unary_real(self, op, expected, a):
         """Tests for 'area', 'length', 'is_valid', etc."""
         fcmp = assert_series_equal
@@ -1346,3 +1393,76 @@ class TestGeomMethods:
             self._test_binary_operator("__sub__", expected, self.g1, self.t2)
         with pytest.warns(FutureWarning):
             self._test_binary_operator("__sub__", expected, self.gdf1, self.t2)
+
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="get_coordinates not implemented for shapely<2",
+    )
+    def test_get_coordinates(self):
+        expected = DataFrame(
+            data=self.expected_2d,
+            columns=["x", "y"],
+            index=[0, 1, 3, 3, 3, 3, 4, 4, 4, 4, 6, 6, 6],
+        )
+        assert_frame_equal(self.g11.get_coordinates(), expected)
+
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="get_coordinates not implemented for shapely<2",
+    )
+    def test_get_coordinates_z(self):
+        expected = DataFrame(
+            data=self.expected_3d,
+            columns=["x", "y", "z"],
+            index=[0, 1, 3, 3, 3, 3, 4, 4, 4, 4, 6, 6, 6],
+        )
+        assert_frame_equal(self.g11.get_coordinates(include_z=True), expected)
+
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="get_coordinates not implemented for shapely<2",
+    )
+    def test_get_coordinates_ignore(self):
+        expected = DataFrame(
+            data=self.expected_2d,
+            columns=["x", "y"],
+        )
+        assert_frame_equal(self.g11.get_coordinates(ignore_index=True), expected)
+
+    @pytest.mark.skipif(
+        not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="get_coordinates not implemented for shapely<2",
+    )
+    def test_get_coordinates_parts(self):
+        expected = DataFrame(
+            data=self.expected_2d,
+            columns=["x", "y"],
+            index=MultiIndex.from_tuples(
+                [
+                    (0, 0),
+                    (1, 0),
+                    (3, 0),
+                    (3, 1),
+                    (3, 2),
+                    (3, 3),
+                    (4, 0),
+                    (4, 1),
+                    (4, 2),
+                    (4, 3),
+                    (6, 0),
+                    (6, 1),
+                    (6, 2),
+                ]
+            ),
+        )
+        assert_frame_equal(self.g11.get_coordinates(index_parts=True), expected)
+
+    @pytest.mark.skipif(
+        (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
+        reason="get_coordinates not implemented for shapely<2",
+    )
+    def test_get_coordinates_not(self):
+        with pytest.raises(
+            NotImplementedError, match="shapely >= 2.0 or PyGEOS are required"
+        ):
+            self.g11.get_coordinates()
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
index 034f2720..2ae1c9f8 100644
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -12,19 +12,16 @@ from geopandas import GeoDataFrame, GeoSeries
 
 class TestMerging:
     def setup_method(self):
-
         self.gseries = GeoSeries([Point(i, i) for i in range(3)])
         self.series = pd.Series([1, 2, 3])
         self.gdf = GeoDataFrame({"geometry": self.gseries, "values": range(3)})
         self.df = pd.DataFrame({"col1": [1, 2, 3], "col2": [0.1, 0.2, 0.3]})
 
     def _check_metadata(self, gdf, geometry_column_name="geometry", crs=None):
-
         assert gdf._geometry_column_name == geometry_column_name
         assert gdf.crs == crs
 
     def test_merge(self):
-
         res = self.gdf.merge(self.df, left_on="values", right_on="col1")
 
         # check result is a GeoDataFrame
@@ -63,7 +60,6 @@ class TestMerging:
         assert isinstance(res.geometry, GeoSeries)
 
     def test_concat_axis0_crs(self):
-
         # CRS not set for both GeoDataFrame
         res = pd.concat([self.gdf, self.gdf])
         self._check_metadata(res)
@@ -138,7 +134,6 @@ class TestMerging:
             pd.concat([single_geom_col, partial_none_case])
 
     def test_concat_axis1(self):
-
         res = pd.concat([self.gdf, self.df], axis=1)
 
         assert res.shape == (3, 4)
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
index 026c0551..60a3f85f 100644
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -405,7 +405,6 @@ def test_correct_index(dfs):
 
 
 def test_warn_on_keep_geom_type(dfs):
-
     df1, df2 = dfs
     polys3 = GeoSeries(
         [
@@ -720,7 +719,6 @@ def test_overlap_make_valid(make_valid):
 
 
 def test_empty_overlay_return_non_duplicated_columns():
-
     nybb = geopandas.read_file(geopandas.datasets.get_path("nybb"))
     nybb2 = nybb.copy()
     nybb2.geometry = nybb2.translate(20000000)
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 10cae2e5..5f0a7180 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -105,7 +105,6 @@ def test_repr_linearring():
 
 
 def test_indexing(s, df):
-
     # accessing scalar from the geometry (column)
     exp = Point(1, 1)
     assert s[1] == exp
@@ -243,7 +242,6 @@ def test_assign(df):
 
 
 def test_astype(s, df):
-
     # check geoseries functionality
     with pytest.raises(TypeError):
         s.astype(int)
@@ -313,7 +311,6 @@ def test_convert_dtypes(df):
 
 
 def test_to_csv(df):
-
     exp = (
         "geometry,value1,value2\nPOINT (0 0),0,1\nPOINT (1 1),1,2\nPOINT (2 2),2,1\n"
     ).replace("\n", os.linesep)
@@ -474,8 +471,14 @@ def test_value_counts():
     # each object is considered unique
     s = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)])
     res = s.value_counts()
+    if compat.PANDAS_GE_20:
+        name = "count"
+    else:
+        name = None
     with compat.ignore_shapely2_warnings():
-        exp = pd.Series([2, 1], index=pd14_compat_index([Point(0, 0), Point(1, 1)]))
+        exp = pd.Series(
+            [2, 1], index=pd14_compat_index([Point(0, 0), Point(1, 1)]), name=name
+        )
     assert_series_equal(res, exp)
     # Check crs doesn't make a difference - note it is not kept in output index anyway
     s2 = GeoSeries([Point(0, 0), Point(1, 1), Point(0, 0)], crs="EPSG:4326")
@@ -490,17 +493,19 @@ def test_value_counts():
     res3 = s3.value_counts()
     index = pd14_compat_index([Point(0, 0), LineString([[1, 1], [2, 2]])])
     with compat.ignore_shapely2_warnings():
-        exp3 = pd.Series([2, 1], index=index)
+        exp3 = pd.Series([2, 1], index=index, name=name)
     assert_series_equal(res3, exp3)
 
     # check None is handled
     s4 = GeoSeries([Point(0, 0), None, Point(0, 0)])
     res4 = s4.value_counts(dropna=True)
     with compat.ignore_shapely2_warnings():
-        exp4_dropna = pd.Series([2], index=pd14_compat_index([Point(0, 0)]))
+        exp4_dropna = pd.Series([2], index=pd14_compat_index([Point(0, 0)]), name=name)
     assert_series_equal(res4, exp4_dropna)
     with compat.ignore_shapely2_warnings():
-        exp4_keepna = pd.Series([2, 1], index=pd14_compat_index([Point(0, 0), None]))
+        exp4_keepna = pd.Series(
+            [2, 1], index=pd14_compat_index([Point(0, 0), None]), name=name
+        )
     res4_keepna = s4.value_counts(dropna=False)
     assert_series_equal(res4_keepna, exp4_keepna)
 
@@ -530,7 +535,6 @@ def test_drop_duplicates_frame():
 
 
 def test_groupby(df):
-
     # counts work fine
     res = df.groupby("value2").count()
     exp = pd.DataFrame(
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 52925c97..e4d288eb 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -70,7 +70,6 @@ class TestPointPlotting:
         )
 
     def test_figsize(self):
-
         ax = self.points.plot(figsize=(1, 1))
         np.testing.assert_array_equal(ax.figure.get_size_inches(), (1, 1))
 
@@ -78,7 +77,6 @@ class TestPointPlotting:
         np.testing.assert_array_equal(ax.figure.get_size_inches(), (1, 1))
 
     def test_default_colors(self):
-
         # # without specifying values -> uniform color
 
         # GeoSeries
@@ -100,7 +98,6 @@ class TestPointPlotting:
         _check_colors(self.N, ax.collections[0].get_facecolors(), expected_colors)
 
     def test_series_color_no_index(self):
-
         # Color order with ordered index
         colors_ord = pd.Series(["a", "b", "c", "a", "b", "c", "a", "b", "c", "a"])
 
@@ -117,7 +114,6 @@ class TestPointPlotting:
         np.testing.assert_array_equal(point_colors1[1], point_colors2[1])
 
     def test_series_color_index(self):
-
         # Color order with out-of-order index
         colors_ord = pd.Series(
             ["a", "a", "a", "a", "b", "b", "b", "c", "c", "c"],
@@ -137,7 +133,6 @@ class TestPointPlotting:
         np.testing.assert_array_equal(point_colors1[1], point_colors2[1])
 
     def test_colormap(self):
-
         # without specifying values but cmap specified -> no uniform color
         # but different colors for all points
 
@@ -163,7 +158,6 @@ class TestPointPlotting:
         _check_colors(self.N, ax.collections[0].get_facecolors(), exp_colors)
 
     def test_single_color(self):
-
         ax = self.points.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_facecolors(), ["green"] * self.N)
 
@@ -190,7 +184,6 @@ class TestPointPlotting:
             )
 
     def test_markersize(self):
-
         ax = self.points.plot(markersize=10)
         assert ax.collections[0].get_sizes() == [10]
 
@@ -214,7 +207,6 @@ class TestPointPlotting:
         )
 
     def test_style_kwargs(self):
-
         ax = self.points.plot(edgecolors="k")
         assert (ax.collections[0].get_edgecolor() == [0, 0, 0, 1]).all()
 
@@ -290,7 +282,6 @@ class TestPointPlotting:
         np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
 
     def test_empty_plot(self):
-
         s = GeoSeries([Polygon()])
         with pytest.warns(UserWarning):
             ax = s.plot()
@@ -305,7 +296,6 @@ class TestPointPlotting:
         assert len(ax.collections) == 0
 
     def test_empty_geometry(self):
-
         if compat.USE_PYGEOS:
             s = GeoSeries([wkt.loads("POLYGON EMPTY")])
             s = GeoSeries(
@@ -361,7 +351,6 @@ class TestPointPlotting:
             np.testing.assert_array_equal(ax.get_children()[0].get_facecolor()[0], blue)
 
     def test_multipoints(self):
-
         # MultiPoints
         ax = self.df2.plot()
         _check_colors(4, ax.collections[0].get_facecolors(), [MPL_DFT_COLOR] * 4)
@@ -502,7 +491,6 @@ class TestLineStringPlotting:
         self.df3 = GeoDataFrame({"geometry": self.linearrings, "values": values})
 
     def test_single_color(self):
-
         ax = self.lines.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
 
@@ -612,7 +600,6 @@ class TestLineStringPlotting:
         np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
 
     def test_multilinestrings(self):
-
         # MultiLineStrings
         ax = self.df2.plot()
         assert len(ax.collections[0].get_paths()) == 4
@@ -631,7 +618,6 @@ class TestLineStringPlotting:
 
 class TestPolygonPlotting:
     def setup_method(self):
-
         t1 = Polygon([(0, 0), (1, 0), (1, 1)])
         t2 = Polygon([(1, 0), (2, 0), (2, 1)])
         self.polys = GeoSeries([t1, t2], index=list("AB"))
@@ -648,7 +634,6 @@ class TestPolygonPlotting:
         self.df3 = pd.concat([self.df, df_nan])
 
     def test_single_color(self):
-
         ax = self.polys.plot(color="green")
         _check_colors(2, ax.collections[0].get_facecolors(), ["green"] * 2)
         # color only sets facecolor
@@ -690,7 +675,6 @@ class TestPolygonPlotting:
         assert np.any(np.not_equal(actual_colors[0], actual_colors[1]))
 
     def test_style_kwargs_color(self):
-
         # facecolor overrides default cmap when color is not set
         ax = self.polys.plot(facecolor="k")
         _check_colors(2, ax.collections[0].get_facecolors(), ["k"] * 2)
@@ -765,7 +749,6 @@ class TestPolygonPlotting:
             np.testing.assert_array_equal([0.7, 0.2], ax.collections[0].get_alpha())
 
     def test_legend_kwargs(self):
-
         ax = self.df.plot(
             column="values",
             categorical=True,
@@ -812,7 +795,6 @@ class TestPolygonPlotting:
         self.df.plot(column="values", legend=True, legend_kwds={"fmt": "{:.0f}"})
 
     def test_multipolygons_color(self):
-
         # MultiPolygons
         ax = self.df2.plot()
         assert len(ax.collections[0].get_paths()) == 4
@@ -886,7 +868,6 @@ class TestPolygonPlotting:
 
 class TestPolygonZPlotting:
     def setup_method(self):
-
         t1 = Polygon([(0, 0, 0), (1, 0, 0), (1, 1, 1)])
         t2 = Polygon([(1, 0, 0), (2, 0, 0), (2, 1, 1)])
         self.polys = GeoSeries([t1, t2], index=list("AB"))
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index 8299f6b2..1b15d21e 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -543,7 +543,6 @@ class TestSpatialJoinNYBB:
         assert sjoin(empty, self.pointdf, how="left", predicate=predicate).empty
 
     def test_empty_sjoin_return_duplicated_columns(self):
-
         nybb = geopandas.read_file(geopandas.datasets.get_path("nybb"))
         nybb2 = nybb.copy()
         nybb2.geometry = nybb2.translate(200000)  # to get non-overlapping