that's too much!

This commit is contained in:
2024-12-19 20:22:56 -08:00
parent 0020a609dd
commit 32cd60e92b
8443 changed files with 1446950 additions and 42 deletions

View File

@@ -0,0 +1,462 @@
"""Tests for the clip module."""
import numpy as np
import shapely
from shapely.geometry import (
Polygon,
Point,
LineString,
LinearRing,
GeometryCollection,
MultiPoint,
box,
)
import geopandas
from geopandas import GeoDataFrame, GeoSeries, clip
from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
import pytest
from geopandas.tools.clip import _mask_is_list_like_rectangle
pytestmark = pytest.mark.skip_no_sindex
mask_variants_single_rectangle = [
"single_rectangle_gdf",
"single_rectangle_gdf_list_bounds",
"single_rectangle_gdf_tuple_bounds",
"single_rectangle_gdf_array_bounds",
]
mask_variants_large_rectangle = [
"larger_single_rectangle_gdf",
"larger_single_rectangle_gdf_bounds",
]
@pytest.fixture
def point_gdf():
"""Create a point GeoDataFrame."""
pts = np.array([[2, 2], [3, 4], [9, 8], [-12, -15]])
gdf = GeoDataFrame([Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857")
return gdf
@pytest.fixture
def pointsoutside_nooverlap_gdf():
"""Create a point GeoDataFrame. Its points are all outside the single
rectangle, and its bounds are outside the single rectangle's."""
pts = np.array([[5, 15], [15, 15], [15, 20]])
gdf = GeoDataFrame([Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857")
return gdf
@pytest.fixture
def pointsoutside_overlap_gdf():
"""Create a point GeoDataFrame. Its points are all outside the single
rectangle, and its bounds are overlapping the single rectangle's."""
pts = np.array([[5, 15], [15, 15], [15, 5]])
gdf = GeoDataFrame([Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857")
return gdf
@pytest.fixture
def single_rectangle_gdf():
"""Create a single rectangle for clipping."""
poly_inters = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
gdf = GeoDataFrame([1], geometry=[poly_inters], crs="EPSG:3857")
gdf["attr2"] = "site-boundary"
return gdf
@pytest.fixture
def single_rectangle_gdf_tuple_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return tuple(single_rectangle_gdf.total_bounds)
@pytest.fixture
def single_rectangle_gdf_list_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return list(single_rectangle_gdf.total_bounds)
@pytest.fixture
def single_rectangle_gdf_array_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return single_rectangle_gdf.total_bounds
@pytest.fixture
def larger_single_rectangle_gdf():
"""Create a slightly larger rectangle for clipping.
The smaller single rectangle is used to test the edge case where slivers
are returned when you clip polygons. This fixture is larger which
eliminates the slivers in the clip return.
"""
poly_inters = Polygon([(-5, -5), (-5, 15), (15, 15), (15, -5), (-5, -5)])
gdf = GeoDataFrame([1], geometry=[poly_inters], crs="EPSG:3857")
gdf["attr2"] = ["study area"]
return gdf
@pytest.fixture
def larger_single_rectangle_gdf_bounds(larger_single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return tuple(larger_single_rectangle_gdf.total_bounds)
@pytest.fixture
def buffered_locations(point_gdf):
"""Buffer points to create a multi-polygon."""
buffered_locs = point_gdf
buffered_locs["geometry"] = buffered_locs.buffer(4)
buffered_locs["type"] = "plot"
return buffered_locs
@pytest.fixture
def donut_geometry(buffered_locations, single_rectangle_gdf):
"""Make a geometry with a hole in the middle (a donut)."""
donut = geopandas.overlay(
buffered_locations, single_rectangle_gdf, how="symmetric_difference"
)
return donut
@pytest.fixture
def two_line_gdf():
"""Create Line Objects For Testing"""
linea = LineString([(1, 1), (2, 2), (3, 2), (5, 3)])
lineb = LineString([(3, 4), (5, 7), (12, 2), (10, 5), (9, 7.5)])
gdf = GeoDataFrame([1, 2], geometry=[linea, lineb], crs="EPSG:3857")
return gdf
@pytest.fixture
def multi_poly_gdf(donut_geometry):
"""Create a multi-polygon GeoDataFrame."""
multi_poly = donut_geometry.unary_union
out_df = GeoDataFrame(geometry=GeoSeries(multi_poly), crs="EPSG:3857")
out_df["attr"] = ["pool"]
return out_df
@pytest.fixture
def multi_line(two_line_gdf):
"""Create a multi-line GeoDataFrame.
This GDF has one multiline and one regular line."""
# Create a single and multi line object
multiline_feat = two_line_gdf.unary_union
linec = LineString([(2, 1), (3, 1), (4, 1), (5, 2)])
out_df = GeoDataFrame(geometry=GeoSeries([multiline_feat, linec]), crs="EPSG:3857")
out_df["attr"] = ["road", "stream"]
return out_df
@pytest.fixture
def multi_point(point_gdf):
"""Create a multi-point GeoDataFrame."""
multi_point = point_gdf.unary_union
out_df = GeoDataFrame(
geometry=GeoSeries(
[multi_point, Point(2, 5), Point(-11, -14), Point(-10, -12)]
),
crs="EPSG:3857",
)
out_df["attr"] = ["tree", "another tree", "shrub", "berries"]
return out_df
@pytest.fixture
def mixed_gdf():
"""Create a Mixed Polygon and LineString For Testing"""
point = Point(2, 3)
line = LineString([(1, 1), (2, 2), (3, 2), (5, 3), (12, 1)])
poly = Polygon([(3, 4), (5, 2), (12, 2), (10, 5), (9, 7.5)])
ring = LinearRing([(1, 1), (2, 2), (3, 2), (5, 3), (12, 1)])
gdf = GeoDataFrame(
[1, 2, 3, 4], geometry=[point, poly, line, ring], crs="EPSG:3857"
)
return gdf
@pytest.fixture
def geomcol_gdf():
"""Create a Mixed Polygon and LineString For Testing"""
point = Point(2, 3)
poly = Polygon([(3, 4), (5, 2), (12, 2), (10, 5), (9, 7.5)])
coll = GeometryCollection([point, poly])
gdf = GeoDataFrame([1], geometry=[coll], crs="EPSG:3857")
return gdf
@pytest.fixture
def sliver_line():
"""Create a line that will create a point when clipped."""
linea = LineString([(10, 5), (13, 5), (15, 5)])
lineb = LineString([(1, 1), (2, 2), (3, 2), (5, 3), (12, 1)])
gdf = GeoDataFrame([1, 2], geometry=[linea, lineb], crs="EPSG:3857")
return gdf
def test_not_gdf(single_rectangle_gdf):
"""Non-GeoDataFrame inputs raise attribute errors."""
with pytest.raises(TypeError):
clip((2, 3), single_rectangle_gdf)
with pytest.raises(TypeError):
clip(single_rectangle_gdf, "foobar")
with pytest.raises(TypeError):
clip(single_rectangle_gdf, (1, 2, 3))
with pytest.raises(TypeError):
clip(single_rectangle_gdf, (1, 2, 3, 4, 5))
def test_non_overlapping_geoms():
"""Test that a bounding box returns empty if the extents don't overlap"""
unit_box = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
unit_gdf = GeoDataFrame([1], geometry=[unit_box], crs="EPSG:3857")
non_overlapping_gdf = unit_gdf.copy()
non_overlapping_gdf = non_overlapping_gdf.geometry.apply(
lambda x: shapely.affinity.translate(x, xoff=20)
)
out = clip(unit_gdf, non_overlapping_gdf)
assert_geodataframe_equal(out, unit_gdf.iloc[:0])
out2 = clip(unit_gdf.geometry, non_overlapping_gdf)
assert_geoseries_equal(out2, GeoSeries(crs=unit_gdf.crs))
@pytest.mark.parametrize("mask_fixture_name", mask_variants_single_rectangle)
class TestClipWithSingleRectangleGdf:
@pytest.fixture
def mask(self, mask_fixture_name, request):
return request.getfixturevalue(mask_fixture_name)
def test_returns_gdf(self, point_gdf, mask):
"""Test that function returns a GeoDataFrame (or GDF-like) object."""
out = clip(point_gdf, mask)
assert isinstance(out, GeoDataFrame)
def test_returns_series(self, point_gdf, mask):
"""Test that function returns a GeoSeries if GeoSeries is passed."""
out = clip(point_gdf.geometry, mask)
assert isinstance(out, GeoSeries)
def test_clip_points(self, point_gdf, mask):
"""Test clipping a points GDF with a generic polygon geometry."""
clip_pts = clip(point_gdf, mask)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame(
[Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857"
)
assert_geodataframe_equal(clip_pts, exp)
def test_clip_points_geom_col_rename(self, point_gdf, mask):
"""Test clipping a points GDF with a generic polygon geometry."""
point_gdf_geom_col_rename = point_gdf.rename_geometry("geometry2")
clip_pts = clip(point_gdf_geom_col_rename, mask)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame(
[Point(xy) for xy in pts],
columns=["geometry2"],
crs="EPSG:3857",
geometry="geometry2",
)
assert_geodataframe_equal(clip_pts, exp)
def test_clip_poly(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations, mask)
assert len(clipped_poly.geometry) == 3
assert all(clipped_poly.geom_type == "Polygon")
def test_clip_poly_geom_col_rename(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
poly_gdf_geom_col_rename = buffered_locations.rename_geometry("geometry2")
clipped_poly = clip(poly_gdf_geom_col_rename, mask)
assert len(clipped_poly.geometry) == 3
assert "geometry" not in clipped_poly.keys()
assert "geometry2" in clipped_poly.keys()
def test_clip_poly_series(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations.geometry, mask)
assert len(clipped_poly) == 3
assert all(clipped_poly.geom_type == "Polygon")
def test_clip_multipoly_keep_geom_type(self, multi_poly_gdf, mask):
"""Test a multi poly object where the return includes a sliver.
Also the bounds of the object should == the bounds of the clip object
if they fully overlap (as they do in these fixtures)."""
clipped = clip(multi_poly_gdf, mask, keep_geom_type=True)
expected_bounds = (
mask if _mask_is_list_like_rectangle(mask) else mask.total_bounds
)
assert np.array_equal(clipped.total_bounds, expected_bounds)
# Assert returned data is a not geometry collection
assert (clipped.geom_type.isin(["Polygon", "MultiPolygon"])).all()
def test_clip_multiline(self, multi_line, mask):
"""Test that clipping a multiline feature with a poly returns expected
output."""
clipped = clip(multi_line, mask)
assert clipped.geom_type[0] == "MultiLineString"
def test_clip_multipoint(self, multi_point, mask):
"""Clipping a multipoint feature with a polygon works as expected.
should return a geodataframe with a single multi point feature"""
clipped = clip(multi_point, mask)
assert clipped.geom_type[0] == "MultiPoint"
assert hasattr(clipped, "attr")
# All points should intersect the clip geom
assert len(clipped) == 2
clipped_mutltipoint = MultiPoint(
[
Point(2, 2),
Point(3, 4),
Point(9, 8),
]
)
assert clipped.iloc[0].geometry.wkt == clipped_mutltipoint.wkt
shape_for_points = (
box(*mask) if _mask_is_list_like_rectangle(mask) else mask.unary_union
)
assert all(clipped.intersects(shape_for_points))
def test_clip_lines(self, two_line_gdf, mask):
"""Test what happens when you give the clip_extent a line GDF."""
clip_line = clip(two_line_gdf, mask)
assert len(clip_line.geometry) == 2
def test_mixed_geom(self, mixed_gdf, mask):
"""Test clipping a mixed GeoDataFrame"""
clipped = clip(mixed_gdf, mask)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_mixed_series(self, mixed_gdf, mask):
"""Test clipping a mixed GeoSeries"""
clipped = clip(mixed_gdf.geometry, mask)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_clip_with_line_extra_geom(self, sliver_line, mask):
"""When the output of a clipped line returns a geom collection,
and keep_geom_type is True, no geometry collections should be returned."""
clipped = clip(sliver_line, mask, keep_geom_type=True)
assert len(clipped.geometry) == 1
# Assert returned data is a not geometry collection
assert not (clipped.geom_type == "GeometryCollection").any()
def test_clip_no_box_overlap(self, pointsoutside_nooverlap_gdf, mask):
"""Test clip when intersection is empty and boxes do not overlap."""
clipped = clip(pointsoutside_nooverlap_gdf, mask)
assert len(clipped) == 0
def test_clip_box_overlap(self, pointsoutside_overlap_gdf, mask):
"""Test clip when intersection is empty and boxes do overlap."""
clipped = clip(pointsoutside_overlap_gdf, mask)
assert len(clipped) == 0
def test_warning_extra_geoms_mixed(self, mixed_gdf, mask):
"""Test the correct warnings are raised if keep_geom_type is
called on a mixed GDF"""
with pytest.warns(UserWarning):
clip(mixed_gdf, mask, keep_geom_type=True)
def test_warning_geomcoll(self, geomcol_gdf, mask):
"""Test the correct warnings are raised if keep_geom_type is
called on a GDF with GeometryCollection"""
with pytest.warns(UserWarning):
clip(geomcol_gdf, mask, keep_geom_type=True)
def test_clip_line_keep_slivers(sliver_line, single_rectangle_gdf):
"""Test the correct output if a point is returned
from a line only geometry type."""
clipped = clip(sliver_line, single_rectangle_gdf)
# Assert returned data is a geometry collection given sliver geoms
assert "Point" == clipped.geom_type[0]
assert "LineString" == clipped.geom_type[1]
def test_clip_multipoly_keep_slivers(multi_poly_gdf, single_rectangle_gdf):
"""Test a multi poly object where the return includes a sliver.
Also the bounds of the object should == the bounds of the clip object
if they fully overlap (as they do in these fixtures)."""
clipped = clip(multi_poly_gdf, single_rectangle_gdf)
assert np.array_equal(clipped.total_bounds, single_rectangle_gdf.total_bounds)
# Assert returned data is a geometry collection given sliver geoms
assert "GeometryCollection" in clipped.geom_type[0]
def test_warning_crs_mismatch(point_gdf, single_rectangle_gdf):
with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
clip(point_gdf, single_rectangle_gdf.to_crs(4326))
def test_clip_with_polygon(single_rectangle_gdf):
"""Test clip when using a shapely object"""
polygon = Polygon([(0, 0), (5, 12), (10, 0), (0, 0)])
clipped = clip(single_rectangle_gdf, polygon)
exp_poly = polygon.intersection(
Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
)
exp = GeoDataFrame([1], geometry=[exp_poly], crs="EPSG:3857")
exp["attr2"] = "site-boundary"
assert_geodataframe_equal(clipped, exp)
def test_clip_with_multipolygon(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon with a multipolygon."""
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(single_rectangle_gdf, multi)
assert clipped.geom_type[0] == "Polygon"
@pytest.mark.parametrize(
"mask_fixture_name",
mask_variants_large_rectangle,
)
def test_clip_single_multipoly_no_extra_geoms(
buffered_locations, mask_fixture_name, request
):
"""When clipping a multi-polygon feature, no additional geom types
should be returned."""
masks = request.getfixturevalue(mask_fixture_name)
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(multi, masks)
assert clipped.geom_type[0] == "Polygon"
@pytest.mark.filterwarnings("ignore:All-NaN slice encountered")
@pytest.mark.parametrize(
"mask",
[
Polygon(),
(np.nan,) * 4,
(np.nan, 0, np.nan, 1),
GeoSeries([Polygon(), Polygon()], crs="EPSG:3857"),
GeoSeries([Polygon(), Polygon()], crs="EPSG:3857").to_frame(),
GeoSeries([], crs="EPSG:3857"),
GeoSeries([], crs="EPSG:3857").to_frame(),
],
)
def test_clip_empty_mask(buffered_locations, mask):
"""Test that clipping with empty mask returns an empty result."""
clipped = clip(buffered_locations, mask)
assert_geodataframe_equal(
clipped,
GeoDataFrame([], columns=["geometry", "type"], crs="EPSG:3857"),
check_index_type=False,
)
clipped = clip(buffered_locations.geometry, mask)
assert_geoseries_equal(clipped, GeoSeries([], crs="EPSG:3857"))

View File

@@ -0,0 +1,75 @@
import numpy as np
from shapely.geometry import Point
from shapely.wkt import loads
import geopandas
import pytest
from pandas.testing import assert_series_equal
def test_hilbert_distance():
# test the actual Hilbert Code algorithm against some hardcoded values
geoms = geopandas.GeoSeries.from_wkt(
[
"POINT (0 0)",
"POINT (1 1)",
"POINT (1 0)",
"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
]
)
result = geoms.hilbert_distance(total_bounds=(0, 0, 1, 1), level=2)
assert result.tolist() == [0, 10, 15, 2]
result = geoms.hilbert_distance(total_bounds=(0, 0, 1, 1), level=3)
assert result.tolist() == [0, 42, 63, 10]
result = geoms.hilbert_distance(total_bounds=(0, 0, 1, 1), level=16)
assert result.tolist() == [0, 2863311530, 4294967295, 715827882]
@pytest.fixture
def geoseries_points():
p1 = Point(1, 2)
p2 = Point(2, 3)
p3 = Point(3, 4)
p4 = Point(4, 1)
return geopandas.GeoSeries([p1, p2, p3, p4])
def test_hilbert_distance_level(geoseries_points):
with pytest.raises(ValueError):
geoseries_points.hilbert_distance(level=20)
def test_specified_total_bounds(geoseries_points):
result = geoseries_points.hilbert_distance(
total_bounds=geoseries_points.total_bounds
)
expected = geoseries_points.hilbert_distance()
assert_series_equal(result, expected)
@pytest.mark.parametrize(
"empty",
[
None,
loads("POLYGON EMPTY"),
],
)
def test_empty(geoseries_points, empty):
s = geoseries_points
s.iloc[-1] = empty
with pytest.raises(
ValueError, match="cannot be computed on a GeoSeries with empty"
):
s.hilbert_distance()
def test_zero_width():
# special case of all points on the same line -> avoid warnings because
# of division by 0 and introducing NaN
s = geopandas.GeoSeries([Point(0, 0), Point(0, 2), Point(0, 1)])
with np.errstate(all="raise"):
result = s.hilbert_distance()
assert np.array(result).argsort().tolist() == [0, 2, 1]

View File

@@ -0,0 +1,57 @@
import pytest
import numpy
import geopandas
import geopandas._compat as compat
from geopandas.tools._random import uniform
multipolygons = geopandas.read_file(geopandas.datasets.get_path("nybb")).geometry
polygons = multipolygons.explode(ignore_index=True).geometry
multilinestrings = multipolygons.boundary
linestrings = polygons.boundary
points = multipolygons.centroid
@pytest.mark.skipif(
not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
reason="array input in interpolate not implemented for shapely<2",
)
@pytest.mark.parametrize("size", [10, 100])
@pytest.mark.parametrize(
"geom", [multipolygons[0], polygons[0], multilinestrings[0], linestrings[0]]
)
def test_uniform(geom, size):
sample = uniform(geom, size=size, rng=1)
sample_series = geopandas.GeoSeries(sample).explode().reset_index(drop=True)
assert len(sample_series) == size
sample_in_geom = sample_series.buffer(0.00000001).sindex.query(
geom, predicate="intersects"
)
assert len(sample_in_geom) == size
@pytest.mark.skipif(
not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
reason="array input in interpolate not implemented for shapely<2",
)
def test_uniform_unsupported():
with pytest.warns(UserWarning, match="Sampling is not supported"):
sample = uniform(points[0], size=10, rng=1)
assert sample.is_empty
@pytest.mark.skipif(
not (compat.USE_PYGEOS or compat.USE_SHAPELY_20),
reason="array input in interpolate not implemented for shapely<2",
)
def test_uniform_generator():
sample = uniform(polygons[0], size=10, rng=1)
sample2 = uniform(polygons[0], size=10, rng=1)
assert sample.equals(sample2)
generator = numpy.random.default_rng(seed=1)
gen_sample = uniform(polygons[0], size=10, rng=generator)
gen_sample2 = uniform(polygons[0], size=10, rng=generator)
assert sample.equals(gen_sample)
assert not sample.equals(gen_sample2)

View File

@@ -0,0 +1,960 @@
import math
from typing import Sequence
import numpy as np
import pandas as pd
import shapely
from shapely.geometry import Point, Polygon, GeometryCollection
import geopandas
import geopandas._compat as compat
from geopandas import GeoDataFrame, GeoSeries, read_file, sjoin, sjoin_nearest
from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
from pandas.testing import assert_frame_equal, assert_series_equal
import pytest
TEST_NEAREST = compat.USE_SHAPELY_20 or (compat.PYGEOS_GE_010 and compat.USE_PYGEOS)
pytestmark = pytest.mark.skip_no_sindex
@pytest.fixture()
def dfs(request):
polys1 = GeoSeries(
[
Polygon([(0, 0), (5, 0), (5, 5), (0, 5)]),
Polygon([(5, 5), (6, 5), (6, 6), (5, 6)]),
Polygon([(6, 0), (9, 0), (9, 3), (6, 3)]),
]
)
polys2 = GeoSeries(
[
Polygon([(1, 1), (4, 1), (4, 4), (1, 4)]),
Polygon([(4, 4), (7, 4), (7, 7), (4, 7)]),
Polygon([(7, 7), (10, 7), (10, 10), (7, 10)]),
]
)
df1 = GeoDataFrame({"geometry": polys1, "df1": [0, 1, 2]})
df2 = GeoDataFrame({"geometry": polys2, "df2": [3, 4, 5]})
if request.param == "string-index":
df1.index = ["a", "b", "c"]
df2.index = ["d", "e", "f"]
if request.param == "named-index":
df1.index.name = "df1_ix"
df2.index.name = "df2_ix"
if request.param == "multi-index":
i1 = ["a", "b", "c"]
i2 = ["d", "e", "f"]
df1 = df1.set_index([i1, i2])
df2 = df2.set_index([i2, i1])
if request.param == "named-multi-index":
i1 = ["a", "b", "c"]
i2 = ["d", "e", "f"]
df1 = df1.set_index([i1, i2])
df2 = df2.set_index([i2, i1])
df1.index.names = ["df1_ix1", "df1_ix2"]
df2.index.names = ["df2_ix1", "df2_ix2"]
# construction expected frames
expected = {}
part1 = df1.copy().reset_index().rename(columns={"index": "index_left"})
part2 = (
df2.copy()
.iloc[[0, 1, 1, 2]]
.reset_index()
.rename(columns={"index": "index_right"})
)
part1["_merge"] = [0, 1, 2]
part2["_merge"] = [0, 0, 1, 3]
exp = pd.merge(part1, part2, on="_merge", how="outer")
expected["intersects"] = exp.drop("_merge", axis=1).copy()
part1 = df1.copy().reset_index().rename(columns={"index": "index_left"})
part2 = df2.copy().reset_index().rename(columns={"index": "index_right"})
part1["_merge"] = [0, 1, 2]
part2["_merge"] = [0, 3, 3]
exp = pd.merge(part1, part2, on="_merge", how="outer")
expected["contains"] = exp.drop("_merge", axis=1).copy()
part1["_merge"] = [0, 1, 2]
part2["_merge"] = [3, 1, 3]
exp = pd.merge(part1, part2, on="_merge", how="outer")
expected["within"] = exp.drop("_merge", axis=1).copy()
return [request.param, df1, df2, expected]
class TestSpatialJoin:
@pytest.mark.parametrize(
"how, lsuffix, rsuffix, expected_cols",
[
("left", "left", "right", {"col_left", "col_right", "index_right"}),
("inner", "left", "right", {"col_left", "col_right", "index_right"}),
("right", "left", "right", {"col_left", "col_right", "index_left"}),
("left", "lft", "rgt", {"col_lft", "col_rgt", "index_rgt"}),
("inner", "lft", "rgt", {"col_lft", "col_rgt", "index_rgt"}),
("right", "lft", "rgt", {"col_lft", "col_rgt", "index_lft"}),
],
)
def test_suffixes(self, how: str, lsuffix: str, rsuffix: str, expected_cols):
left = GeoDataFrame({"col": [1], "geometry": [Point(0, 0)]})
right = GeoDataFrame({"col": [1], "geometry": [Point(0, 0)]})
joined = sjoin(left, right, how=how, lsuffix=lsuffix, rsuffix=rsuffix)
assert set(joined.columns) == expected_cols | {"geometry"}
@pytest.mark.parametrize("dfs", ["default-index", "string-index"], indirect=True)
def test_crs_mismatch(self, dfs):
index, df1, df2, expected = dfs
df1.crs = "epsg:4326"
with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
sjoin(df1, df2)
@pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
@pytest.mark.parametrize("op", ["intersects", "contains", "within"])
def test_deprecated_op_param(self, dfs, op):
_, df1, df2, _ = dfs
with pytest.warns(FutureWarning, match="`op` parameter is deprecated"):
sjoin(df1, df2, op=op)
@pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
@pytest.mark.parametrize("op", ["intersects", "contains", "within"])
@pytest.mark.parametrize("predicate", ["contains", "within"])
def test_deprecated_op_param_nondefault_predicate(self, dfs, op, predicate):
_, df1, df2, _ = dfs
match = "use the `predicate` parameter instead"
if op != predicate:
warntype = UserWarning
match = (
"`predicate` will be overridden by the value of `op`" # noqa: ISC003
+ r"(.|\s)*"
+ match
)
else:
warntype = FutureWarning
with pytest.warns(warntype, match=match):
sjoin(df1, df2, predicate=predicate, op=op)
@pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
def test_unknown_kwargs(self, dfs):
_, df1, df2, _ = dfs
with pytest.raises(
TypeError,
match=r"sjoin\(\) got an unexpected keyword argument 'extra_param'",
):
sjoin(df1, df2, extra_param="test")
@pytest.mark.filterwarnings("ignore:The `op` parameter:FutureWarning")
@pytest.mark.parametrize(
"dfs",
[
"default-index",
"string-index",
"named-index",
"multi-index",
"named-multi-index",
],
indirect=True,
)
@pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
@pytest.mark.parametrize("predicate_kw", ["predicate", "op"])
def test_inner(self, predicate, predicate_kw, dfs):
index, df1, df2, expected = dfs
res = sjoin(df1, df2, how="inner", **{predicate_kw: predicate})
exp = expected[predicate].dropna().copy()
exp = exp.drop("geometry_y", axis=1).rename(columns={"geometry_x": "geometry"})
exp[["df1", "df2"]] = exp[["df1", "df2"]].astype("int64")
if index == "default-index":
exp[["index_left", "index_right"]] = exp[
["index_left", "index_right"]
].astype("int64")
if index == "named-index":
exp[["df1_ix", "df2_ix"]] = exp[["df1_ix", "df2_ix"]].astype("int64")
exp = exp.set_index("df1_ix").rename(columns={"df2_ix": "index_right"})
if index in ["default-index", "string-index"]:
exp = exp.set_index("index_left")
exp.index.name = None
if index == "multi-index":
exp = exp.set_index(["level_0_x", "level_1_x"]).rename(
columns={"level_0_y": "index_right0", "level_1_y": "index_right1"}
)
exp.index.names = df1.index.names
if index == "named-multi-index":
exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
)
exp.index.names = df1.index.names
assert_frame_equal(res, exp)
@pytest.mark.parametrize(
"dfs",
[
"default-index",
"string-index",
"named-index",
"multi-index",
"named-multi-index",
],
indirect=True,
)
@pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
def test_left(self, predicate, dfs):
index, df1, df2, expected = dfs
res = sjoin(df1, df2, how="left", predicate=predicate)
if index in ["default-index", "string-index"]:
exp = expected[predicate].dropna(subset=["index_left"]).copy()
elif index == "named-index":
exp = expected[predicate].dropna(subset=["df1_ix"]).copy()
elif index == "multi-index":
exp = expected[predicate].dropna(subset=["level_0_x"]).copy()
elif index == "named-multi-index":
exp = expected[predicate].dropna(subset=["df1_ix1"]).copy()
exp = exp.drop("geometry_y", axis=1).rename(columns={"geometry_x": "geometry"})
exp["df1"] = exp["df1"].astype("int64")
if index == "default-index":
exp["index_left"] = exp["index_left"].astype("int64")
# TODO: in result the dtype is object
res["index_right"] = res["index_right"].astype(float)
elif index == "named-index":
exp[["df1_ix"]] = exp[["df1_ix"]].astype("int64")
exp = exp.set_index("df1_ix").rename(columns={"df2_ix": "index_right"})
if index in ["default-index", "string-index"]:
exp = exp.set_index("index_left")
exp.index.name = None
if index == "multi-index":
exp = exp.set_index(["level_0_x", "level_1_x"]).rename(
columns={"level_0_y": "index_right0", "level_1_y": "index_right1"}
)
exp.index.names = df1.index.names
if index == "named-multi-index":
exp = exp.set_index(["df1_ix1", "df1_ix2"]).rename(
columns={"df2_ix1": "index_right0", "df2_ix2": "index_right1"}
)
exp.index.names = df1.index.names
assert_frame_equal(res, exp)
def test_empty_join(self):
# Check joins resulting in empty gdfs.
polygons = geopandas.GeoDataFrame(
{
"col2": [1, 2],
"geometry": [
Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
Polygon([(1, 0), (2, 0), (2, 1), (1, 1)]),
],
}
)
not_in = geopandas.GeoDataFrame({"col1": [1], "geometry": [Point(-0.5, 0.5)]})
empty = sjoin(not_in, polygons, how="left", predicate="intersects")
assert empty.index_right.isnull().all()
empty = sjoin(not_in, polygons, how="right", predicate="intersects")
assert empty.index_left.isnull().all()
empty = sjoin(not_in, polygons, how="inner", predicate="intersects")
assert empty.empty
@pytest.mark.parametrize(
"predicate",
[
"contains",
"contains_properly",
"covered_by",
"covers",
"crosses",
"intersects",
"touches",
"within",
],
)
@pytest.mark.parametrize(
"empty",
[
GeoDataFrame(geometry=[GeometryCollection(), GeometryCollection()]),
GeoDataFrame(geometry=GeoSeries()),
],
)
def test_join_with_empty(self, predicate, empty):
# Check joins with empty geometry columns/dataframes.
polygons = geopandas.GeoDataFrame(
{
"col2": [1, 2],
"geometry": [
Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
Polygon([(1, 0), (2, 0), (2, 1), (1, 1)]),
],
}
)
result = sjoin(empty, polygons, how="left", predicate=predicate)
assert result.index_right.isnull().all()
result = sjoin(empty, polygons, how="right", predicate=predicate)
assert result.index_left.isnull().all()
result = sjoin(empty, polygons, how="inner", predicate=predicate)
assert result.empty
@pytest.mark.parametrize("dfs", ["default-index", "string-index"], indirect=True)
def test_sjoin_invalid_args(self, dfs):
index, df1, df2, expected = dfs
with pytest.raises(ValueError, match="'left_df' should be GeoDataFrame"):
sjoin(df1.geometry, df2)
with pytest.raises(ValueError, match="'right_df' should be GeoDataFrame"):
sjoin(df1, df2.geometry)
@pytest.mark.parametrize(
"dfs",
[
"default-index",
"string-index",
"named-index",
"multi-index",
"named-multi-index",
],
indirect=True,
)
@pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
def test_right(self, predicate, dfs):
index, df1, df2, expected = dfs
res = sjoin(df1, df2, how="right", predicate=predicate)
if index in ["default-index", "string-index"]:
exp = expected[predicate].dropna(subset=["index_right"]).copy()
elif index == "named-index":
exp = expected[predicate].dropna(subset=["df2_ix"]).copy()
elif index == "multi-index":
exp = expected[predicate].dropna(subset=["level_0_y"]).copy()
elif index == "named-multi-index":
exp = expected[predicate].dropna(subset=["df2_ix1"]).copy()
exp = exp.drop("geometry_x", axis=1).rename(columns={"geometry_y": "geometry"})
exp["df2"] = exp["df2"].astype("int64")
if index == "default-index":
exp["index_right"] = exp["index_right"].astype("int64")
res["index_left"] = res["index_left"].astype(float)
elif index == "named-index":
exp[["df2_ix"]] = exp[["df2_ix"]].astype("int64")
exp = exp.set_index("df2_ix").rename(columns={"df1_ix": "index_left"})
if index in ["default-index", "string-index"]:
exp = exp.set_index("index_right")
exp = exp.reindex(columns=res.columns)
exp.index.name = None
if index == "multi-index":
exp = exp.set_index(["level_0_y", "level_1_y"]).rename(
columns={"level_0_x": "index_left0", "level_1_x": "index_left1"}
)
exp.index.names = df2.index.names
if index == "named-multi-index":
exp = exp.set_index(["df2_ix1", "df2_ix2"]).rename(
columns={"df1_ix1": "index_left0", "df1_ix2": "index_left1"}
)
exp.index.names = df2.index.names
if predicate == "within":
exp = exp.sort_index()
assert_frame_equal(res, exp, check_index_type=False)
class TestSpatialJoinNYBB:
def setup_method(self):
nybb_filename = geopandas.datasets.get_path("nybb")
self.polydf = read_file(nybb_filename)
self.crs = self.polydf.crs
N = 20
b = [int(x) for x in self.polydf.total_bounds]
self.pointdf = GeoDataFrame(
[
{"geometry": Point(x, y), "pointattr1": x + y, "pointattr2": x - y}
for x, y in zip(
range(b[0], b[2], int((b[2] - b[0]) / N)),
range(b[1], b[3], int((b[3] - b[1]) / N)),
)
],
crs=self.crs,
)
def test_geometry_name(self):
# test sjoin is working with other geometry name
polydf_original_geom_name = self.polydf.geometry.name
self.polydf = self.polydf.rename(columns={"geometry": "new_geom"}).set_geometry(
"new_geom"
)
assert polydf_original_geom_name != self.polydf.geometry.name
res = sjoin(self.polydf, self.pointdf, how="left")
assert self.polydf.geometry.name == res.geometry.name
def test_sjoin_left(self):
df = sjoin(self.pointdf, self.polydf, how="left")
assert df.shape == (21, 8)
for i, row in df.iterrows():
assert row.geometry.geom_type == "Point"
assert "pointattr1" in df.columns
assert "BoroCode" in df.columns
def test_sjoin_right(self):
# the inverse of left
df = sjoin(self.pointdf, self.polydf, how="right")
df2 = sjoin(self.polydf, self.pointdf, how="left")
assert df.shape == (12, 8)
assert df.shape == df2.shape
for i, row in df.iterrows():
assert row.geometry.geom_type == "MultiPolygon"
for i, row in df2.iterrows():
assert row.geometry.geom_type == "MultiPolygon"
def test_sjoin_inner(self):
df = sjoin(self.pointdf, self.polydf, how="inner")
assert df.shape == (11, 8)
def test_sjoin_predicate(self):
# points within polygons
df = sjoin(self.pointdf, self.polydf, how="left", predicate="within")
assert df.shape == (21, 8)
assert df.loc[1]["BoroName"] == "Staten Island"
# points contain polygons? never happens so we should have nulls
df = sjoin(self.pointdf, self.polydf, how="left", predicate="contains")
assert df.shape == (21, 8)
assert np.isnan(df.loc[1]["Shape_Area"])
def test_sjoin_bad_predicate(self):
# AttributeError: 'Point' object has no attribute 'spandex'
with pytest.raises(ValueError):
sjoin(self.pointdf, self.polydf, how="left", predicate="spandex")
def test_sjoin_duplicate_column_name(self):
pointdf2 = self.pointdf.rename(columns={"pointattr1": "Shape_Area"})
df = sjoin(pointdf2, self.polydf, how="left")
assert "Shape_Area_left" in df.columns
assert "Shape_Area_right" in df.columns
@pytest.mark.parametrize("how", ["left", "right", "inner"])
def test_sjoin_named_index(self, how):
# original index names should be unchanged
pointdf2 = self.pointdf.copy()
pointdf2.index.name = "pointid"
polydf = self.polydf.copy()
polydf.index.name = "polyid"
res = sjoin(pointdf2, polydf, how=how)
assert pointdf2.index.name == "pointid"
assert polydf.index.name == "polyid"
# original index name should pass through to result
if how == "right":
assert res.index.name == "polyid"
else: # how == "left", how == "inner"
assert res.index.name == "pointid"
def test_sjoin_values(self):
# GH190
self.polydf.index = [1, 3, 4, 5, 6]
df = sjoin(self.pointdf, self.polydf, how="left")
assert df.shape == (21, 8)
df = sjoin(self.polydf, self.pointdf, how="left")
assert df.shape == (12, 8)
@pytest.mark.xfail
def test_no_overlapping_geometry(self):
# Note: these tests are for correctly returning GeoDataFrame
# when result of the join is empty
df_inner = sjoin(self.pointdf.iloc[17:], self.polydf, how="inner")
df_left = sjoin(self.pointdf.iloc[17:], self.polydf, how="left")
df_right = sjoin(self.pointdf.iloc[17:], self.polydf, how="right")
expected_inner_df = pd.concat(
[
self.pointdf.iloc[:0],
pd.Series(name="index_right", dtype="int64"),
self.polydf.drop("geometry", axis=1).iloc[:0],
],
axis=1,
)
expected_inner = GeoDataFrame(expected_inner_df)
expected_right_df = pd.concat(
[
self.pointdf.drop("geometry", axis=1).iloc[:0],
pd.concat(
[
pd.Series(name="index_left", dtype="int64"),
pd.Series(name="index_right", dtype="int64"),
],
axis=1,
),
self.polydf,
],
axis=1,
)
expected_right = GeoDataFrame(expected_right_df).set_index("index_right")
expected_left_df = pd.concat(
[
self.pointdf.iloc[17:],
pd.Series(name="index_right", dtype="int64"),
self.polydf.iloc[:0].drop("geometry", axis=1),
],
axis=1,
)
expected_left = GeoDataFrame(expected_left_df)
assert expected_inner.equals(df_inner)
assert expected_right.equals(df_right)
assert expected_left.equals(df_left)
@pytest.mark.skip("Not implemented")
def test_sjoin_outer(self):
df = sjoin(self.pointdf, self.polydf, how="outer")
assert df.shape == (21, 8)
def test_sjoin_empty_geometries(self):
# https://github.com/geopandas/geopandas/issues/944
empty = GeoDataFrame(geometry=[GeometryCollection()] * 3)
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")
assert df2.shape == (21, 8)
@pytest.mark.parametrize("predicate", ["intersects", "within", "contains"])
def test_sjoin_no_valid_geoms(self, predicate):
"""Tests a completely empty GeoDataFrame."""
empty = GeoDataFrame(geometry=[], crs=self.pointdf.crs)
assert sjoin(self.pointdf, empty, how="inner", predicate=predicate).empty
assert sjoin(self.pointdf, empty, how="right", predicate=predicate).empty
assert sjoin(empty, self.pointdf, how="inner", predicate=predicate).empty
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
result = geopandas.sjoin(nybb, nybb2)
assert "BoroCode_right" in result.columns
assert "BoroCode_left" in result.columns
class TestSpatialJoinNaturalEarth:
def setup_method(self):
world_path = geopandas.datasets.get_path("naturalearth_lowres")
cities_path = geopandas.datasets.get_path("naturalearth_cities")
self.world = read_file(world_path)
self.cities = read_file(cities_path)
def test_sjoin_inner(self):
# GH637
countries = self.world[["geometry", "name"]]
countries = countries.rename(columns={"name": "country"})
cities_with_country = sjoin(
self.cities, countries, how="inner", predicate="intersects"
)
assert cities_with_country.shape == (213, 4)
@pytest.mark.skipif(
TEST_NEAREST,
reason=("This test can only be run _without_ PyGEOS >= 0.10 installed"),
)
def test_no_nearest_all():
df1 = geopandas.GeoDataFrame({"geometry": []})
df2 = geopandas.GeoDataFrame({"geometry": []})
with pytest.raises(
NotImplementedError,
match="Currently, only PyGEOS >= 0.10.0 or Shapely >= 2.0 supports",
):
sjoin_nearest(df1, df2)
@pytest.mark.skipif(
not TEST_NEAREST,
reason=(
"PyGEOS >= 0.10.0"
" must be installed and activated via the geopandas.compat module to"
" test sjoin_nearest"
),
)
class TestNearest:
@pytest.mark.parametrize(
"how_kwargs", ({}, {"how": "inner"}, {"how": "left"}, {"how": "right"})
)
def test_allowed_hows(self, how_kwargs):
left = geopandas.GeoDataFrame({"geometry": []})
right = geopandas.GeoDataFrame({"geometry": []})
sjoin_nearest(left, right, **how_kwargs) # no error
@pytest.mark.parametrize("how", ("outer", "abcde"))
def test_invalid_hows(self, how: str):
left = geopandas.GeoDataFrame({"geometry": []})
right = geopandas.GeoDataFrame({"geometry": []})
with pytest.raises(ValueError, match="`how` was"):
sjoin_nearest(left, right, how=how)
@pytest.mark.parametrize("distance_col", (None, "distance"))
def test_empty_right_df_how_left(self, distance_col: str):
# all records from left and no results from right
left = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
right = geopandas.GeoDataFrame({"geometry": []})
joined = sjoin_nearest(
left,
right,
how="left",
distance_col=distance_col,
)
assert_geoseries_equal(joined["geometry"], left["geometry"])
assert joined["index_right"].isna().all()
if distance_col is not None:
assert joined[distance_col].isna().all()
@pytest.mark.parametrize("distance_col", (None, "distance"))
def test_empty_right_df_how_right(self, distance_col: str):
# no records in joined
left = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
right = geopandas.GeoDataFrame({"geometry": []})
joined = sjoin_nearest(
left,
right,
how="right",
distance_col=distance_col,
)
assert joined.empty
if distance_col is not None:
assert distance_col in joined
@pytest.mark.parametrize("how", ["inner", "left"])
@pytest.mark.parametrize("distance_col", (None, "distance"))
def test_empty_left_df(self, how, distance_col: str):
right = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
left = geopandas.GeoDataFrame({"geometry": []})
joined = sjoin_nearest(left, right, how=how, distance_col=distance_col)
assert joined.empty
if distance_col is not None:
assert distance_col in joined
@pytest.mark.parametrize("distance_col", (None, "distance"))
def test_empty_left_df_how_right(self, distance_col: str):
right = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
left = geopandas.GeoDataFrame({"geometry": []})
joined = sjoin_nearest(
left,
right,
how="right",
distance_col=distance_col,
)
assert_geoseries_equal(joined["geometry"], right["geometry"])
assert joined["index_left"].isna().all()
if distance_col is not None:
assert joined[distance_col].isna().all()
@pytest.mark.parametrize("how", ["inner", "left"])
def test_empty_join_due_to_max_distance(self, how):
# after applying max_distance the join comes back empty
# (as in NaN in the joined columns)
left = geopandas.GeoDataFrame({"geometry": [Point(0, 0)]})
right = geopandas.GeoDataFrame({"geometry": [Point(1, 1), Point(2, 2)]})
joined = sjoin_nearest(
left,
right,
how=how,
max_distance=1,
distance_col="distances",
)
expected = left.copy()
expected["index_right"] = [np.nan]
expected["distances"] = [np.nan]
if how == "inner":
expected = expected.dropna()
expected["index_right"] = expected["index_right"].astype("int64")
assert_geodataframe_equal(joined, expected)
def test_empty_join_due_to_max_distance_how_right(self):
# after applying max_distance the join comes back empty
# (as in NaN in the joined columns)
left = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
right = geopandas.GeoDataFrame({"geometry": [Point(2, 2)]})
joined = sjoin_nearest(
left,
right,
how="right",
max_distance=1,
distance_col="distances",
)
expected = right.copy()
expected["index_left"] = [np.nan]
expected["distances"] = [np.nan]
expected = expected[["index_left", "geometry", "distances"]]
assert_geodataframe_equal(joined, expected)
@pytest.mark.parametrize("how", ["inner", "left"])
def test_max_distance(self, how):
left = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
right = geopandas.GeoDataFrame({"geometry": [Point(1, 1), Point(2, 2)]})
joined = sjoin_nearest(
left,
right,
how=how,
max_distance=1,
distance_col="distances",
)
expected = left.copy()
expected["index_right"] = [np.nan, 0]
expected["distances"] = [np.nan, 0]
if how == "inner":
expected = expected.dropna()
expected["index_right"] = expected["index_right"].astype("int64")
assert_geodataframe_equal(joined, expected)
def test_max_distance_how_right(self):
left = geopandas.GeoDataFrame({"geometry": [Point(1, 1), Point(2, 2)]})
right = geopandas.GeoDataFrame({"geometry": [Point(0, 0), Point(1, 1)]})
joined = sjoin_nearest(
left,
right,
how="right",
max_distance=1,
distance_col="distances",
)
expected = right.copy()
expected["index_left"] = [np.nan, 0]
expected["distances"] = [np.nan, 0]
expected = expected[["index_left", "geometry", "distances"]]
assert_geodataframe_equal(joined, expected)
@pytest.mark.parametrize("how", ["inner", "left"])
@pytest.mark.parametrize(
"geo_left, geo_right, expected_left, expected_right, distances",
[
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1)],
[0, 1],
[0, 0],
[math.sqrt(2), 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0)],
[0, 1],
[1, 0],
[0, 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0), Point(0, 0)],
[0, 0, 1],
[1, 2, 0],
[0, 0, 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0), Point(2, 2)],
[0, 1],
[1, 0],
[0, 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0.25, 1)],
[0, 1],
[1, 0],
[math.sqrt(0.25**2 + 1), 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(-10, -10), Point(100, 100)],
[0, 1],
[0, 0],
[math.sqrt(10**2 + 10**2), math.sqrt(11**2 + 11**2)],
),
(
[Point(0, 0), Point(1, 1)],
[Point(x, y) for x, y in zip(np.arange(10), np.arange(10))],
[0, 1],
[0, 1],
[0, 0],
),
(
[Point(0, 0), Point(1, 1), Point(0, 0)],
[Point(1.1, 1.1), Point(0, 0)],
[0, 1, 2],
[1, 0, 1],
[0, np.sqrt(0.1**2 + 0.1**2), 0],
),
],
)
def test_sjoin_nearest_left(
self,
geo_left,
geo_right,
expected_left: Sequence[int],
expected_right: Sequence[int],
distances: Sequence[float],
how,
):
left = geopandas.GeoDataFrame({"geometry": geo_left})
right = geopandas.GeoDataFrame({"geometry": geo_right})
expected_gdf = left.iloc[expected_left].copy()
expected_gdf["index_right"] = expected_right
# without distance col
joined = sjoin_nearest(left, right, how=how)
# inner / left join give a different row order
check_like = how == "inner"
assert_geodataframe_equal(expected_gdf, joined, check_like=check_like)
# with distance col
expected_gdf["distance_col"] = np.array(distances, dtype=float)
joined = sjoin_nearest(left, right, how=how, distance_col="distance_col")
assert_geodataframe_equal(expected_gdf, joined, check_like=check_like)
@pytest.mark.parametrize(
"geo_left, geo_right, expected_left, expected_right, distances",
[
([Point(0, 0), Point(1, 1)], [Point(1, 1)], [1], [0], [0]),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0)],
[1, 0],
[0, 1],
[0, 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0), Point(0, 0)],
[1, 0, 0],
[0, 1, 2],
[0, 0, 0],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0, 0), Point(2, 2)],
[1, 0, 1],
[0, 1, 2],
[0, 0, math.sqrt(2)],
),
(
[Point(0, 0), Point(1, 1)],
[Point(1, 1), Point(0.25, 1)],
[1, 1],
[0, 1],
[0, 0.75],
),
(
[Point(0, 0), Point(1, 1)],
[Point(-10, -10), Point(100, 100)],
[0, 1],
[0, 1],
[math.sqrt(10**2 + 10**2), math.sqrt(99**2 + 99**2)],
),
(
[Point(0, 0), Point(1, 1)],
[Point(x, y) for x, y in zip(np.arange(10), np.arange(10))],
[0, 1] + [1] * 8,
list(range(10)),
[0, 0] + [np.sqrt(x**2 + x**2) for x in np.arange(1, 9)],
),
(
[Point(0, 0), Point(1, 1), Point(0, 0)],
[Point(1.1, 1.1), Point(0, 0)],
[1, 0, 2],
[0, 1, 1],
[np.sqrt(0.1**2 + 0.1**2), 0, 0],
),
],
)
def test_sjoin_nearest_right(
self,
geo_left,
geo_right,
expected_left: Sequence[int],
expected_right: Sequence[int],
distances: Sequence[float],
):
left = geopandas.GeoDataFrame({"geometry": geo_left})
right = geopandas.GeoDataFrame({"geometry": geo_right})
expected_gdf = right.iloc[expected_right].copy()
expected_gdf["index_left"] = expected_left
expected_gdf = expected_gdf[["index_left", "geometry"]]
# without distance col
joined = sjoin_nearest(left, right, how="right")
assert_geodataframe_equal(expected_gdf, joined)
# with distance col
expected_gdf["distance_col"] = np.array(distances, dtype=float)
joined = sjoin_nearest(left, right, how="right", distance_col="distance_col")
assert_geodataframe_equal(expected_gdf, joined)
@pytest.mark.filterwarnings("ignore:Geometry is in a geographic CRS")
def test_sjoin_nearest_inner(self):
# check equivalency of left and inner join
countries = read_file(geopandas.datasets.get_path("naturalearth_lowres"))
cities = read_file(geopandas.datasets.get_path("naturalearth_cities"))
countries = countries[["geometry", "name"]].rename(columns={"name": "country"})
# default: inner and left give the same result
result1 = sjoin_nearest(cities, countries, distance_col="dist")
assert result1.shape[0] == cities.shape[0]
result2 = sjoin_nearest(cities, countries, distance_col="dist", how="inner")
assert_geodataframe_equal(result2, result1)
result3 = sjoin_nearest(cities, countries, distance_col="dist", how="left")
assert_geodataframe_equal(result3, result1, check_like=True)
# with max_distance: rows that go above are dropped in case of inner
result4 = sjoin_nearest(cities, countries, distance_col="dist", max_distance=1)
assert_geodataframe_equal(
result4, result1[result1["dist"] < 1], check_like=True
)
result5 = sjoin_nearest(
cities, countries, distance_col="dist", max_distance=1, how="left"
)
assert result5.shape[0] == cities.shape[0]
result5 = result5.dropna()
result5["index_right"] = result5["index_right"].astype("int64")
assert_geodataframe_equal(result5, result4, check_like=True)
expected_index_uncapped = (
[1, 3, 3, 1, 2] if compat.PANDAS_GE_22 else [1, 1, 3, 3, 2]
)
@pytest.mark.skipif(
not (compat.USE_SHAPELY_20),
reason=(
"shapely >= 2.0 is required to run sjoin_nearest"
"with parameter `exclusive` set"
),
)
@pytest.mark.parametrize(
"max_distance,expected", [(None, expected_index_uncapped), (1.1, [3, 3, 1, 2])]
)
def test_sjoin_nearest_exclusive(self, max_distance, expected):
geoms = shapely.points(np.arange(3), np.arange(3))
geoms = np.append(geoms, [Point(1, 2)])
df = geopandas.GeoDataFrame({"geometry": geoms})
result = df.sjoin_nearest(
df, max_distance=max_distance, distance_col="dist", exclusive=True
)
assert_series_equal(
result["index_right"].reset_index(drop=True),
pd.Series(expected),
check_names=False,
)
if max_distance:
assert result["dist"].max() <= max_distance

View File

@@ -0,0 +1,51 @@
from shapely.geometry import LineString, MultiPoint, Point
from geopandas import GeoSeries
from geopandas.tools import collect
import pytest
class TestTools:
def setup_method(self):
self.p1 = Point(0, 0)
self.p2 = Point(1, 1)
self.p3 = Point(2, 2)
self.mpc = MultiPoint([self.p1, self.p2, self.p3])
self.mp1 = MultiPoint([self.p1, self.p2])
self.line1 = LineString([(3, 3), (4, 4)])
def test_collect_single(self):
result = collect(self.p1)
assert self.p1.equals(result)
def test_collect_single_force_multi(self):
result = collect(self.p1, multi=True)
expected = MultiPoint([self.p1])
assert expected.equals(result)
def test_collect_multi(self):
result = collect(self.mp1)
assert self.mp1.equals(result)
def test_collect_multi_force_multi(self):
result = collect(self.mp1)
assert self.mp1.equals(result)
def test_collect_list(self):
result = collect([self.p1, self.p2, self.p3])
assert self.mpc.equals(result)
def test_collect_GeoSeries(self):
s = GeoSeries([self.p1, self.p2, self.p3])
result = collect(s)
assert self.mpc.equals(result)
def test_collect_mixed_types(self):
with pytest.raises(ValueError):
collect([self.p1, self.line1])
def test_collect_mixed_multi(self):
with pytest.raises(ValueError):
collect([self.mpc, self.mp1])