[Git][debian-gis-team/python-geopandas][upstream] New upstream version 0.14.4
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Sun Apr 28 19:11:28 BST 2024
Bas Couwenberg pushed to branch upstream at Debian GIS Project / python-geopandas
Commits:
9ff1d268 by Bas Couwenberg at 2024-04-28T19:59:50+02:00
New upstream version 0.14.4
- - - - -
27 changed files:
- CHANGELOG.md
- ci/envs/311-dev.yaml
- geopandas/_compat.py
- geopandas/_version.py
- geopandas/array.py
- geopandas/base.py
- geopandas/geodataframe.py
- geopandas/geoseries.py
- geopandas/io/file.py
- geopandas/io/tests/test_file.py
- geopandas/io/tests/test_file_geom_types_drivers.py
- + geopandas/io/util.py
- geopandas/tests/test_array.py
- geopandas/tests/test_crs.py
- geopandas/tests/test_dissolve.py
- geopandas/tests/test_extension_array.py
- geopandas/tests/test_geodataframe.py
- geopandas/tests/test_geom_methods.py
- geopandas/tests/test_geoseries.py
- geopandas/tests/test_overlay.py
- geopandas/tests/test_pandas_methods.py
- geopandas/tests/test_plotting.py
- geopandas/tools/overlay.py
- geopandas/tools/tests/test_random.py
- geopandas/tools/tests/test_sjoin.py
- pyproject.toml
- readthedocs.yml
Changes:
=====================================
CHANGELOG.md
=====================================
@@ -1,5 +1,10 @@
# Changelog
+## Version 0.14.4 (April 28, 2024)
+
+- Several fixes for compatibility with the upcoming pandas 3.0, numpy 2.0 and
+ fiona 1.10 releases.
+
## Version 0.14.3 (Jan 31, 2024)
- Several fixes for compatibility with the latest pandas 2.2 release.
=====================================
ci/envs/311-dev.yaml
=====================================
@@ -5,7 +5,6 @@ dependencies:
- python=3.11
- cython
# required
- - numpy
- pyproj
- geos
- packaging
@@ -25,6 +24,7 @@ dependencies:
- mapclassify>=2.4.0
# dev versions of packages
- --pre --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --extra-index-url https://pypi.fury.io/arrow-nightlies/ --extra-index-url https://pypi.org/simple
+ - numpy>=2.0.0.dev
- fiona
- pandas
- matplotlib
=====================================
geopandas/_compat.py
=====================================
@@ -18,7 +18,8 @@ PANDAS_GE_14 = Version(pd.__version__) >= Version("1.4.0rc0")
PANDAS_GE_15 = Version(pd.__version__) >= Version("1.5.0")
PANDAS_GE_20 = Version(pd.__version__) >= Version("2.0.0")
PANDAS_GE_21 = Version(pd.__version__) >= Version("2.1.0")
-PANDAS_GE_22 = Version(pd.__version__) >= Version("2.2.0.dev0")
+PANDAS_GE_22 = Version(pd.__version__) >= Version("2.2.0")
+PANDAS_GE_30 = Version(pd.__version__) >= Version("3.0.0.dev0")
# -----------------------------------------------------------------------------
=====================================
geopandas/_version.py
=====================================
@@ -25,9 +25,9 @@ def get_keywords() -> Dict[str, str]:
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
- git_refnames = " (tag: v0.14.3, 0.14.x)"
- git_full = "5558c35297a537b05675d236ee550612460299ec"
- git_date = "2024-01-31 20:20:12 +0100"
+ git_refnames = " (tag: v0.14.4, 0.14.x)"
+ git_full = "60c9773e44fff8a35344c2a74431e00c5546a4ee"
+ git_date = "2024-04-28 15:48:09 +0200"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
=====================================
geopandas/array.py
=====================================
@@ -1014,14 +1014,19 @@ class GeometryArray(ExtensionArray):
# TODO with numpy >= 1.15, the 'initial' argument can be used
return np.array([np.nan, np.nan, np.nan, np.nan])
b = self.bounds
- return np.array(
- (
- np.nanmin(b[:, 0]), # minx
- np.nanmin(b[:, 1]), # miny
- np.nanmax(b[:, 2]), # maxx
- np.nanmax(b[:, 3]), # maxy
+ with warnings.catch_warnings():
+ # if all rows are empty geometry / none, nan is expected
+ warnings.filterwarnings(
+ "ignore", r"All-NaN slice encountered", RuntimeWarning
+ )
+ return np.array(
+ (
+ np.nanmin(b[:, 0]), # minx
+ np.nanmin(b[:, 1]), # miny
+ np.nanmax(b[:, 2]), # maxx
+ np.nanmax(b[:, 3]), # maxy
+ )
)
- )
# -------------------------------------------------------------------------
# general array like compat
@@ -1159,7 +1164,13 @@ class GeometryArray(ExtensionArray):
return pd.array(string_values, dtype=pd_dtype)
return string_values.astype(dtype, copy=False)
else:
- return np.array(self, dtype=dtype, copy=copy)
+ # numpy 2.0 makes copy=False case strict (errors if cannot avoid the copy)
+ # -> in that case use `np.asarray` as backwards compatible alternative
+ # for `copy=None` (when requiring numpy 2+, this can be cleaned up)
+ if not copy:
+ return np.asarray(self, dtype=dtype)
+ else:
+ return np.array(self, dtype=dtype, copy=copy)
def isna(self):
"""
@@ -1469,7 +1480,7 @@ class GeometryArray(ExtensionArray):
f"does not support reduction '{name}'"
)
- def __array__(self, dtype=None):
+ def __array__(self, dtype=None, copy=None):
"""
The numpy array interface.
@@ -1477,7 +1488,11 @@ class GeometryArray(ExtensionArray):
-------
values : numpy array
"""
- return to_shapely(self)
+ if compat.USE_PYGEOS:
+ return to_shapely(self)
+ if copy and (dtype is None or dtype == np.dtype("object")):
+ return self._data.copy()
+ return self._data
def _binop(self, other, op):
def convert_values(param):
=====================================
geopandas/base.py
=====================================
@@ -4467,7 +4467,7 @@ def _get_index_for_parts(orig_idx, outer_idx, ignore_index, index_parts):
index_arrays.append(inner_index)
index = pd.MultiIndex.from_arrays(
- index_arrays, names=orig_idx.names + [None]
+ index_arrays, names=list(orig_idx.names) + [None]
)
else:
=====================================
geopandas/geodataframe.py
=====================================
@@ -306,7 +306,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
if inplace:
frame = self
else:
- frame = self.copy()
+ if compat.PANDAS_GE_30:
+ frame = self.copy(deep=False)
+ else:
+ frame = self.copy()
to_remove = None
geo_column_name = self._geometry_column_name
@@ -1947,7 +1950,7 @@ individually so that features may have different properties
return df
# overrides the pandas astype method to ensure the correct return type
- def astype(self, dtype, copy=True, errors="raise", **kwargs):
+ def astype(self, dtype, copy=None, errors="raise", **kwargs):
"""
Cast a pandas object to a specified dtype ``dtype``.
@@ -1960,7 +1963,12 @@ individually so that features may have different properties
-------
GeoDataFrame or DataFrame
"""
- df = super().astype(dtype, copy=copy, errors=errors, **kwargs)
+ if not compat.PANDAS_GE_30 and copy is None:
+ copy = True
+ if copy is not None:
+ kwargs["copy"] = copy
+
+ df = super().astype(dtype, errors=errors, **kwargs)
try:
geoms = df[self._geometry_column_name]
=====================================
geopandas/geoseries.py
=====================================
@@ -208,9 +208,16 @@ class GeoSeries(GeoPandasBase, Series):
"Non geometry data passed to GeoSeries constructor, "
f"received data of dtype '{s.dtype}'"
)
- # try to convert to GeometryArray, if fails return plain Series
+ # extract object-dtype numpy array from pandas Series; with CoW this
+ # gives a read-only array, so we try to set the flag back to writeable
+ data = s.to_numpy()
try:
- data = from_shapely(s.values, crs)
+ data.flags.writeable = True
+ except ValueError:
+ pass
+ # try to convert to GeometryArray
+ try:
+ data = from_shapely(data, crs)
except TypeError:
raise TypeError(
"Non geometry data passed to GeoSeries constructor, "
@@ -778,12 +785,10 @@ class GeoSeries(GeoPandasBase, Series):
"""Alias for `notna` method. See `notna` for more detail."""
return self.notna()
- def fillna(self, value=None, method=None, inplace: bool = False, **kwargs):
+ def fillna(self, value=None, inplace: bool = False, **kwargs):
"""
Fill NA values with geometry (or geometries).
- ``method`` is currently not implemented.
-
Parameters
----------
value : shapely geometry or GeoSeries, default None
@@ -852,7 +857,7 @@ class GeoSeries(GeoPandasBase, Series):
"""
if value is None:
value = GeometryCollection() if compat.SHAPELY_GE_20 else BaseGeometry()
- return super().fillna(value=value, method=method, inplace=inplace, **kwargs)
+ return super().fillna(value=value, inplace=inplace, **kwargs)
def __contains__(self, other) -> bool:
"""Allow tests of the form "geom in s"
=====================================
geopandas/io/file.py
=====================================
@@ -5,6 +5,7 @@ import warnings
import numpy as np
import pandas as pd
+from geopandas.io.util import vsi_path
from pandas.api.types import is_integer_dtype
import pyproj
@@ -55,6 +56,7 @@ def _import_fiona():
FIONA_GE_19 = Version(Version(fiona.__version__).base_version) >= Version(
"1.9.0"
)
+
except ImportError as err:
fiona = False
fiona_import_error = str(err)
@@ -168,16 +170,6 @@ def _is_url(url):
return False
-def _is_zip(path):
- """Check if a given path is a zipfile"""
- parsed = fiona.path.ParsedPath.from_uri(path)
- return (
- parsed.archive.endswith(".zip")
- if parsed.archive
- else parsed.path.endswith(".zip")
- )
-
-
def _read_file(filename, bbox=None, mask=None, rows=None, engine=None, **kwargs):
"""
Returns a GeoDataFrame from a file or URL.
@@ -312,22 +304,7 @@ def _read_file_fiona(
# Opening a file via URL or file-like-object above automatically detects a
# zipped file. In order to match that behavior, attempt to add a zip scheme
# if missing.
- if _is_zip(str(path_or_bytes)):
- parsed = fiona.parse_path(str(path_or_bytes))
- if isinstance(parsed, fiona.path.ParsedPath):
- # If fiona is able to parse the path, we can safely look at the scheme
- # and update it to have a zip scheme if necessary.
- schemes = (parsed.scheme or "").split("+")
- if "zip" not in schemes:
- parsed.scheme = "+".join(["zip"] + schemes)
- path_or_bytes = parsed.name
- elif isinstance(parsed, fiona.path.UnparsedPath) and not str(
- path_or_bytes
- ).startswith("/vsi"):
- # If fiona is unable to parse the path, it might have a Windows drive
- # scheme. Try adding zip:// to the front. If the path starts with "/vsi"
- # it is a legacy GDAL path type, so let it pass unmodified.
- path_or_bytes = "zip://" + parsed.name
+ path_or_bytes = vsi_path(str(path_or_bytes))
if from_bytes:
reader = fiona.BytesCollection
=====================================
geopandas/io/tests/test_file.py
=====================================
@@ -130,7 +130,7 @@ def test_to_file(tmpdir, df_nybb, df_null, driver, ext, engine):
df = GeoDataFrame.from_file(tempfilename, engine=engine)
assert "geometry" in df
assert len(df) == 5
- assert np.alltrue(df["BoroName"].values == df_nybb["BoroName"])
+ assert np.all(df["BoroName"].values == df_nybb["BoroName"])
# Write layer with null geometry out to file
tempfilename = os.path.join(str(tmpdir), "null_geom" + ext)
@@ -139,7 +139,7 @@ def test_to_file(tmpdir, df_nybb, df_null, driver, ext, engine):
df = GeoDataFrame.from_file(tempfilename, engine=engine)
assert "geometry" in df
assert len(df) == 2
- assert np.alltrue(df["Name"].values == df_null["Name"])
+ assert np.all(df["Name"].values == df_null["Name"])
# check the expected driver
assert_correct_driver(tempfilename, ext, engine)
@@ -153,7 +153,7 @@ def test_to_file_pathlib(tmpdir, df_nybb, driver, ext, engine):
df = GeoDataFrame.from_file(temppath, engine=engine)
assert "geometry" in df
assert len(df) == 5
- assert np.alltrue(df["BoroName"].values == df_nybb["BoroName"])
+ assert np.all(df["BoroName"].values == df_nybb["BoroName"])
# check the expected driver
assert_correct_driver(temppath, ext, engine)
@@ -1113,7 +1113,7 @@ def test_write_index_to_file(tmpdir, df_points, driver, ext, engine):
# index as string
df_p = df_points.copy()
df = GeoDataFrame(df_p["value1"], geometry=df_p.geometry)
- df.index = pd.TimedeltaIndex(range(len(df)), "days")
+ df.index = pd.to_timedelta(range(len(df)), unit="days")
# TODO: TimedeltaIndex is an invalid field type
df.index = df.index.astype(str)
do_checks(df, index_is_used=True)
@@ -1121,7 +1121,7 @@ def test_write_index_to_file(tmpdir, df_points, driver, ext, engine):
# unnamed DatetimeIndex
df_p = df_points.copy()
df = GeoDataFrame(df_p["value1"], geometry=df_p.geometry)
- df.index = pd.TimedeltaIndex(range(len(df)), "days") + pd.DatetimeIndex(
+ df.index = pd.to_timedelta(range(len(df)), unit="days") + pd.to_datetime(
["1999-12-27"] * len(df)
)
if driver == "ESRI Shapefile":
=====================================
geopandas/io/tests/test_file_geom_types_drivers.py
=====================================
@@ -244,7 +244,14 @@ def geodataframe(request):
return request.param
- at pytest.fixture(params=["GeoJSON", "ESRI Shapefile", "GPKG", "SQLite"])
+ at pytest.fixture(
+ params=[
+ ("GeoJSON", ".geojson"),
+ ("ESRI Shapefile", ".shp"),
+ ("GPKG", ".gpkg"),
+ ("SQLite", ".sqlite"),
+ ]
+)
def ogr_driver(request):
return request.param
@@ -260,9 +267,10 @@ def engine(request):
def test_to_file_roundtrip(tmpdir, geodataframe, ogr_driver, engine):
- output_file = os.path.join(str(tmpdir), "output_file")
+ driver, ext = ogr_driver
+ output_file = os.path.join(str(tmpdir), "output_file" + ext)
write_kwargs = {}
- if ogr_driver == "SQLite":
+ if driver == "SQLite":
write_kwargs["spatialite"] = True
# This if statement can be removed once minimal fiona version >= 1.8.20
@@ -285,22 +293,20 @@ def test_to_file_roundtrip(tmpdir, geodataframe, ogr_driver, engine):
):
write_kwargs["geometry_type"] = "Point Z"
- expected_error = _expected_error_on(geodataframe, ogr_driver)
+ expected_error = _expected_error_on(geodataframe, driver)
if expected_error:
with pytest.raises(
RuntimeError, match="Failed to write record|Could not add feature to layer"
):
geodataframe.to_file(
- output_file, driver=ogr_driver, engine=engine, **write_kwargs
+ output_file, driver=driver, engine=engine, **write_kwargs
)
else:
- geodataframe.to_file(
- output_file, driver=ogr_driver, engine=engine, **write_kwargs
- )
+ geodataframe.to_file(output_file, driver=driver, engine=engine, **write_kwargs)
reloaded = geopandas.read_file(output_file, engine=engine)
- if ogr_driver == "GeoJSON" and engine == "pyogrio":
+ if driver == "GeoJSON" and engine == "pyogrio":
# For GeoJSON files, the int64 column comes back as int32
reloaded["a"] = reloaded["a"].astype("int64")
=====================================
geopandas/io/util.py
=====================================
@@ -0,0 +1,118 @@
+"""Vendored, cut down version of pyogrio/util.py for use with fiona"""
+
+import re
+import sys
+from urllib.parse import urlparse
+
+
+def vsi_path(path: str) -> str:
+ """
+ Ensure path is a local path or a GDAL-compatible vsi path.
+
+ """
+
+ # path is already in GDAL format
+ if path.startswith("/vsi"):
+ return path
+
+ # Windows drive letters (e.g. "C:\") confuse `urlparse` as they look like
+ # URL schemes
+ if sys.platform == "win32" and re.match("^[a-zA-Z]\\:", path):
+ if not path.split("!")[0].endswith(".zip"):
+ return path
+
+ # prefix then allow to proceed with remaining parsing
+ path = f"zip://{path}"
+
+ path, archive, scheme = _parse_uri(path)
+
+ if scheme or archive or path.endswith(".zip"):
+ return _construct_vsi_path(path, archive, scheme)
+
+ return path
+
+
+# Supported URI schemes and their mapping to GDAL's VSI suffix.
+SCHEMES = {
+ "file": "file",
+ "zip": "zip",
+ "tar": "tar",
+ "gzip": "gzip",
+ "http": "curl",
+ "https": "curl",
+ "ftp": "curl",
+ "s3": "s3",
+ "gs": "gs",
+ "az": "az",
+ "adls": "adls",
+ "adl": "adls", # fsspec uses this
+ "hdfs": "hdfs",
+ "webhdfs": "webhdfs",
+ # GDAL additionally supports oss and swift for remote filesystems, but
+ # those are for now not added as supported URI
+}
+
+CURLSCHEMES = {k for k, v in SCHEMES.items() if v == "curl"}
+
+
+def _parse_uri(path: str):
+ """
+ Parse a URI
+
+ Returns a tuples of (path, archive, scheme)
+
+ path : str
+ Parsed path. Includes the hostname and query string in the case
+ of a URI.
+ archive : str
+ Parsed archive path.
+ scheme : str
+ URI scheme such as "https" or "zip+s3".
+ """
+ parts = urlparse(path)
+
+ # if the scheme is not one of GDAL's supported schemes, return raw path
+ if parts.scheme and not all(p in SCHEMES for p in parts.scheme.split("+")):
+ return path, "", ""
+
+ # we have a URI
+ path = parts.path
+ scheme = parts.scheme or ""
+
+ if parts.query:
+ path += "?" + parts.query
+
+ if parts.scheme and parts.netloc:
+ path = parts.netloc + path
+
+ parts = path.split("!")
+ path = parts.pop() if parts else ""
+ archive = parts.pop() if parts else ""
+ return (path, archive, scheme)
+
+
+def _construct_vsi_path(path, archive, scheme) -> str:
+ """Convert a parsed path to a GDAL VSI path"""
+
+ prefix = ""
+ suffix = ""
+ schemes = scheme.split("+")
+
+ if "zip" not in schemes and (archive.endswith(".zip") or path.endswith(".zip")):
+ schemes.insert(0, "zip")
+
+ if schemes:
+ prefix = "/".join(
+ "vsi{0}".format(SCHEMES[p]) for p in schemes if p and p != "file"
+ )
+
+ if schemes[-1] in CURLSCHEMES:
+ suffix = f"{schemes[-1]}://"
+
+ if prefix:
+ if archive:
+ return "/{}/{}{}/{}".format(prefix, suffix, archive, path.lstrip("/"))
+ else:
+ return "/{}/{}{}".format(prefix, suffix, path)
+
+ return path
=====================================
geopandas/tests/test_array.py
=====================================
@@ -281,6 +281,9 @@ def test_as_array():
("geom_almost_equals", (3,)),
],
)
+# filters required for attr=geom_almost_equals only
+ at pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
+ at pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
def test_predicates_vector_scalar(attr, args):
na_value = False
@@ -320,6 +323,9 @@ def test_predicates_vector_scalar(attr, args):
("geom_almost_equals", (3,)),
],
)
+# filters required for attr=geom_almost_equals only
+ at pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
+ at pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
def test_predicates_vector_vector(attr, args):
na_value = False
empty_value = True if attr == "disjoint" else False
=====================================
geopandas/tests/test_crs.py
=====================================
@@ -1,4 +1,5 @@
import random
+import warnings
import numpy as np
import pandas as pd
@@ -82,6 +83,9 @@ def test_to_crs_dimension_z():
assert result.has_z.all()
+# pyproj + numpy 1.25 trigger warning for single-element array -> recommdation is to
+# ignore the warning for now (https://github.com/pyproj4/pyproj/issues/1307)
+ at pytest.mark.filterwarnings("ignore:Conversion of an array with:DeprecationWarning")
def test_to_crs_dimension_mixed():
s = GeoSeries([Point(1, 2), LineString([(1, 2, 3), (4, 5, 6)])], crs=2056)
result = s.to_crs(epsg=4326)
@@ -150,6 +154,9 @@ def test_transform2(epsg4326, epsg26918):
assert_geodataframe_equal(df, utm, check_less_precise=True, check_crs=False)
+# pyproj + numpy 1.25 trigger warning for single-element array -> recommdation is to
+# ignore the warning for now (https://github.com/pyproj4/pyproj/issues/1307)
+ at pytest.mark.filterwarnings("ignore:Conversion of an array with:DeprecationWarning")
def test_crs_axis_order__always_xy():
df = GeoDataFrame(geometry=[Point(-1683723, 6689139)], crs="epsg:26918")
lonlat = df.to_crs("epsg:4326")
@@ -319,7 +326,11 @@ class TestGeometryArrayCRS:
df.crs = 27700
# geometry column without geometry
- df = GeoDataFrame({"geometry": [Point(0, 1)]}).assign(geometry=[0])
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", "Geometry column does not contain geometry", UserWarning
+ )
+ df = GeoDataFrame({"geometry": [Point(0, 1)]}).assign(geometry=[0])
with pytest.raises(
ValueError,
match="Assigning CRS to a GeoDataFrame without an active geometry",
=====================================
geopandas/tests/test_dissolve.py
=====================================
@@ -95,7 +95,7 @@ def test_mean_dissolve(nybb_polydf, first, expected_mean):
)
# for non pandas "mean", numeric only cannot be applied. Drop columns manually
test2 = nybb_polydf.drop(columns=["BoroName"]).dissolve(
- "manhattan_bronx", aggfunc=np.mean
+ "manhattan_bronx", aggfunc="mean"
)
assert_frame_equal(expected_mean, test, check_column_type=False)
@@ -261,6 +261,7 @@ def test_dissolve_categorical():
# when observed=False we get an additional observation
# that wasn't in the original data
+ none_val = None
expected_gdf_observed_false = geopandas.GeoDataFrame(
{
"cat": pd.Categorical(["a", "a", "b", "b"]),
@@ -268,7 +269,7 @@ def test_dissolve_categorical():
"geometry": geopandas.array.from_wkt(
[
"MULTIPOINT (0 0, 1 1)",
- None,
+ none_val,
"POINT (2 2)",
"POINT (3 3)",
]
=====================================
geopandas/tests/test_extension_array.py
=====================================
@@ -13,12 +13,14 @@ A set of fixtures are defined to provide data for the tests (the fixtures
expected to be available to pytest by the inherited pandas tests).
"""
+
+import itertools
import operator
import numpy as np
from numpy.testing import assert_array_equal
import pandas as pd
-from pandas.testing import assert_series_equal
+from pandas.testing import assert_series_equal, assert_frame_equal
from pandas.tests.extension import base as extension_tests
import shapely.geometry
@@ -357,7 +359,73 @@ class TestConstructors(extension_tests.BaseConstructorsTests):
class TestReshaping(extension_tests.BaseReshapingTests):
- pass
+ # NOTE: this test is copied from pandas/tests/extension/base/reshaping.py
+ # because starting with pandas 3.0 the assert_frame_equal is strict regarding
+ # the exact missing value (None vs NaN)
+ # Our `result` uses None, but the way the `expected` is created results in
+ # NaNs (and specifying to use None as fill value in unstack also does not
+ # help)
+ # -> the only change compared to the upstream test is marked
+ @pytest.mark.parametrize(
+ "index",
+ [
+ # Two levels, uniform.
+ pd.MultiIndex.from_product(([["A", "B"], ["a", "b"]]), names=["a", "b"]),
+ # non-uniform
+ pd.MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "b")]),
+ # three levels, non-uniform
+ pd.MultiIndex.from_product([("A", "B"), ("a", "b", "c"), (0, 1, 2)]),
+ pd.MultiIndex.from_tuples(
+ [
+ ("A", "a", 1),
+ ("A", "b", 0),
+ ("A", "a", 0),
+ ("B", "a", 0),
+ ("B", "c", 1),
+ ]
+ ),
+ ],
+ )
+ @pytest.mark.parametrize("obj", ["series", "frame"])
+ def test_unstack(self, data, index, obj):
+ data = data[: len(index)]
+ if obj == "series":
+ ser = pd.Series(data, index=index)
+ else:
+ ser = pd.DataFrame({"A": data, "B": data}, index=index)
+
+ n = index.nlevels
+ levels = list(range(n))
+ # [0, 1, 2]
+ # [(0,), (1,), (2,), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
+ combinations = itertools.chain.from_iterable(
+ itertools.permutations(levels, i) for i in range(1, n)
+ )
+
+ for level in combinations:
+ result = ser.unstack(level=level)
+ assert all(
+ isinstance(result[col].array, type(data)) for col in result.columns
+ )
+
+ if obj == "series":
+ # We should get the same result with to_frame+unstack+droplevel
+ df = ser.to_frame()
+
+ alt = df.unstack(level=level).droplevel(0, axis=1)
+ assert_frame_equal(result, alt)
+
+ obj_ser = ser.astype(object)
+
+ expected = obj_ser.unstack(level=level, fill_value=data.dtype.na_value)
+ if obj == "series":
+ assert (expected.dtypes == object).all()
+ # <------------ next line is added
+ expected[expected.isna()] = None
+ # ------------->
+
+ result = result.astype(object)
+ assert_frame_equal(result, expected)
class TestGetitem(extension_tests.BaseGetitemTests):
=====================================
geopandas/tests/test_geodataframe.py
=====================================
@@ -997,6 +997,7 @@ class TestDataFrame:
" test sjoin_nearest"
),
)
+ @pytest.mark.filterwarnings("ignore:Geometry is in a geographic CRS:UserWarning")
def test_sjoin_nearest(self, how, max_distance, distance_col):
"""
Basic test for availability of the GeoDataFrame method. Other
@@ -1329,7 +1330,8 @@ class TestConstructor:
):
gdf5["geometry"] = "foo"
assert gdf5._geometry_column_name is None
- gdf3 = gdf.copy().assign(geometry=geo_col)
+ with pytest.warns(FutureWarning, match=match):
+ gdf3 = gdf.copy().assign(geometry=geo_col)
assert gdf3._geometry_column_name == "geometry"
# Check that adding a GeoSeries to a column called "geometry" to a
=====================================
geopandas/tests/test_geom_methods.py
=====================================
@@ -1298,7 +1298,8 @@ class TestGeomMethods:
[
"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
"POLYGON ((2 0, 2 3, 3 3, 3 0, 2 0))",
- ]
+ ],
+ crs=3857,
)
assert np.all(r.normalize().geom_equals_exact(exp, 0.001))
@@ -1333,7 +1334,7 @@ class TestGeomMethods:
def test_minimum_bounding_circle(self):
mbc = self.g1.minimum_bounding_circle()
centers = GeoSeries([Point(0.5, 0.5)] * 2)
- assert np.all(mbc.centroid.geom_almost_equals(centers, 0.001))
+ assert np.all(mbc.centroid.geom_equals_exact(centers, 0.001))
assert_series_equal(
mbc.area,
Series([1.560723, 1.560723]),
=====================================
geopandas/tests/test_geoseries.py
=====================================
@@ -1,7 +1,6 @@
import json
import os
import random
-import re
import shutil
import tempfile
import warnings
@@ -145,27 +144,37 @@ class TestSeries:
exp = pd.Series([False, False], index=["A", "B"])
assert_series_equal(a, exp)
+ @pytest.mark.filterwarnings(r"ignore:The 'geom_almost_equals\(\)':FutureWarning")
def test_geom_almost_equals(self):
# TODO: test decimal parameter
- with pytest.warns(FutureWarning, match=re.escape("The 'geom_almost_equals()'")):
- assert np.all(self.g1.geom_almost_equals(self.g1))
- assert_array_equal(self.g1.geom_almost_equals(self.sq), [False, True])
-
- assert_array_equal(
- self.a1.geom_almost_equals(self.a2, align=True), [False, True, False]
+ assert np.all(self.g1.geom_almost_equals(self.g1))
+ assert_array_equal(self.g1.geom_almost_equals(self.sq), [False, True])
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore",
+ "The indices of the two GeoSeries are different",
+ UserWarning,
)
assert_array_equal(
- self.a1.geom_almost_equals(self.a2, align=False), [False, False]
+ self.a1.geom_almost_equals(self.a2, align=True),
+ [False, True, False],
)
+ assert_array_equal(
+ self.a1.geom_almost_equals(self.a2, align=False), [False, False]
+ )
def test_geom_equals_exact(self):
# TODO: test tolerance parameter
assert np.all(self.g1.geom_equals_exact(self.g1, 0.001))
assert_array_equal(self.g1.geom_equals_exact(self.sq, 0.001), [False, True])
-
- assert_array_equal(
- self.a1.geom_equals_exact(self.a2, 0.001, align=True), [False, True, False]
- )
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", "The indices of the two GeoSeries are different", UserWarning
+ )
+ assert_array_equal(
+ self.a1.geom_equals_exact(self.a2, 0.001, align=True),
+ [False, True, False],
+ )
assert_array_equal(
self.a1.geom_equals_exact(self.a2, 0.001, align=False), [False, False]
)
=====================================
geopandas/tests/test_overlay.py
=====================================
@@ -212,6 +212,10 @@ def test_overlay_nybb(how):
expected.loc[24, "geometry"] = None
result.loc[24, "geometry"] = None
+ # missing values get read as None in read_file for a string column, but
+ # are introduced as NaN by overlay
+ expected["BoroName"] = expected["BoroName"].fillna(np.nan)
+
assert_geodataframe_equal(
result,
expected,
@@ -514,6 +518,12 @@ def test_overlay_strict(how, keep_geom_type, geom_types):
expected = expected.sort_values(cols, axis=0).reset_index(drop=True)
result = result.sort_values(cols, axis=0).reset_index(drop=True)
+ # some columns are all-NaN in the result, but get read as object dtype
+ # column of None values in read_file
+ for col in ["col1", "col3", "col4"]:
+ if col in expected.columns and expected[col].isna().all():
+ expected[col] = expected[col].astype("float64")
+
assert_geodataframe_equal(
result,
expected,
@@ -854,7 +864,7 @@ class TestOverlayWikiExample:
def test_intersection(self):
df_result = overlay(self.layer_a, self.layer_b, how="intersection")
- assert df_result.geom_equals(self.intersection).bool()
+ assert df_result.geom_equals(self.intersection).all()
def test_union(self):
df_result = overlay(self.layer_a, self.layer_b, how="union")
=====================================
geopandas/tests/test_pandas_methods.py
=====================================
@@ -754,6 +754,7 @@ def test_apply_loc_len1(df):
np.testing.assert_allclose(result, expected)
+ at pytest.mark.skipif(compat.PANDAS_GE_30, reason="convert_dtype is removed in pandas 3")
def test_apply_convert_dtypes_keyword(s):
# ensure the convert_dtypes keyword is accepted
if not compat.PANDAS_GE_21:
@@ -879,3 +880,20 @@ def test_preserve_flags(df):
with pytest.raises(ValueError):
pd.concat([df, df])
+
+
+ at pytest.mark.skipif(
+ not compat.SHAPELY_GE_20, reason="ufunc only exists in shapely >= 2"
+)
+def test_ufunc():
+ # this is calling a shapely ufunc, but we currently rely on pandas' implementation
+ # of `__array_ufunc__` to wrap the result back into a GeoSeries
+ ser = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
+ result = shapely.buffer(ser, 2)
+ assert isinstance(result, GeoSeries)
+
+ # ensure the result is still writeable
+ # (https://github.com/geopandas/geopandas/issues/3178)
+ assert result.array._data.flags.writeable
+ result.loc[0] = Point(10, 10)
+ assert result.iloc[0] == Point(10, 10)
=====================================
geopandas/tests/test_plotting.py
=====================================
@@ -324,7 +324,14 @@ class TestPointPlotting:
gdf = GeoDataFrame(geometry=[point, empty_point, point_])
gdf["geometry"] = gdf.intersection(poly)
- gdf.loc[3] = [None]
+ with warnings.catch_warnings():
+ # loc to add row calls concat internally, warning for pandas >=2.1
+ warnings.filterwarnings(
+ "ignore",
+ "The behavior of DataFrame concatenation with empty",
+ FutureWarning,
+ )
+ gdf.loc[3] = [None]
ax = gdf.plot()
assert len(ax.collections) == 1
@@ -1133,6 +1140,9 @@ class TestGeographicAspect:
assert ax3.get_aspect() == 0.5
+ at pytest.mark.filterwarnings(
+ "ignore:Numba not installed. Using slow pure python version.:UserWarning"
+)
class TestMapclassifyPlotting:
@classmethod
def setup_class(cls):
=====================================
geopandas/tools/overlay.py
=====================================
@@ -6,6 +6,7 @@ import pandas as pd
from geopandas import GeoDataFrame, GeoSeries
from geopandas.array import _check_crs, _crs_mismatch_warn
+from geopandas._compat import PANDAS_GE_30
def _ensure_geometry_column(df):
@@ -14,12 +15,15 @@ def _ensure_geometry_column(df):
If another column with that name exists, it will be dropped.
"""
if not df._geometry_column_name == "geometry":
- if "geometry" in df.columns:
- df.drop("geometry", axis=1, inplace=True)
- df.rename(
- columns={df._geometry_column_name: "geometry"}, copy=False, inplace=True
- )
- df.set_geometry("geometry", inplace=True)
+ if PANDAS_GE_30:
+ if "geometry" in df.columns:
+ df = df.drop("geometry", axis=1)
+ df = df.rename_geometry("geometry")
+ else:
+ if "geometry" in df.columns:
+ df.drop("geometry", axis=1, inplace=True)
+ df.rename_geometry("geometry", inplace=True)
+ return df
def _overlay_intersection(df1, df2):
@@ -112,8 +116,8 @@ def _overlay_symmetric_diff(df1, df2):
dfdiff1["__idx2"] = np.nan
dfdiff2["__idx1"] = np.nan
# ensure geometry name (otherwise merge goes wrong)
- _ensure_geometry_column(dfdiff1)
- _ensure_geometry_column(dfdiff2)
+ dfdiff1 = _ensure_geometry_column(dfdiff1)
+ dfdiff2 = _ensure_geometry_column(dfdiff2)
# combine both 'difference' dataframes
dfsym = dfdiff1.merge(
dfdiff2, on=["__idx1", "__idx2"], how="outer", suffixes=("_1", "_2")
@@ -136,7 +140,14 @@ def _overlay_union(df1, df2):
"""
dfinter = _overlay_intersection(df1, df2)
dfsym = _overlay_symmetric_diff(df1, df2)
- dfunion = pd.concat([dfinter, dfsym], ignore_index=True, sort=False)
+ with warnings.catch_warnings():
+ # pandas GH52532 FutureWarning, fix new behaviour if needed when it is added
+ warnings.filterwarnings(
+ "ignore",
+ "The behavior of DataFrame concatenation with empty",
+ FutureWarning,
+ )
+ dfunion = pd.concat([dfinter, dfsym], ignore_index=True, sort=False)
# keep geometry column last
columns = list(dfunion.columns)
columns.remove("geometry")
=====================================
geopandas/tools/tests/test_random.py
=====================================
@@ -22,7 +22,9 @@ points = multipolygons.centroid
)
def test_uniform(geom, size):
sample = uniform(geom, size=size, rng=1)
- sample_series = geopandas.GeoSeries(sample).explode().reset_index(drop=True)
+ sample_series = (
+ geopandas.GeoSeries(sample).explode(index_parts=True).reset_index(drop=True)
+ )
assert len(sample_series) == size
sample_in_geom = sample_series.buffer(0.00000001).sindex.query(
geom, predicate="intersects"
=====================================
geopandas/tools/tests/test_sjoin.py
=====================================
@@ -527,7 +527,7 @@ class TestSpatialJoinNYBB:
def test_sjoin_empty_geometries(self):
# https://github.com/geopandas/geopandas/issues/944
- empty = GeoDataFrame(geometry=[GeometryCollection()] * 3)
+ empty = GeoDataFrame(geometry=[GeometryCollection()] * 3, crs=self.crs)
df = sjoin(pd.concat([self.pointdf, empty]), self.polydf, how="left")
assert df.shape == (24, 8)
df2 = sjoin(self.pointdf, pd.concat([self.polydf, empty]), how="left")
=====================================
pyproject.toml
=====================================
@@ -22,6 +22,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"fiona >= 1.8.21",
+ "numpy >= 1.22",
"packaging",
"pandas >= 1.4.0",
"pyproj >= 3.3.0",
=====================================
readthedocs.yml
=====================================
@@ -1,8 +1,13 @@
version: 2
build:
- os: ubuntu-20.04
+ os: ubuntu-22.04
tools:
- python: mambaforge-4.10
+ python: mambaforge-latest
+ jobs:
+ post_checkout:
+ # we need the tags for versioneer to work
+ - git fetch origin --depth 150
+ - git fetch --tags
python:
install:
- method: pip
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-geopandas/-/commit/9ff1d268dafd45d55ee44a1007ebff3d49b35949
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-geopandas/-/commit/9ff1d268dafd45d55ee44a1007ebff3d49b35949
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20240428/9bd49609/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list