ENH: support how="inner" in sjoin_nearest (#2143)

This commit is contained in:
Joris Van den Bossche
2021-09-30 16:08:33 +02:00
committed by GitHub
parent 448334ac1f
commit a05080fa0e
3 changed files with 67 additions and 31 deletions
+2 -3
View File
@@ -145,9 +145,8 @@ Nearest Joins
Proximity-based joins can be done via :func:`sjoin_nearest`.
:func:`sjoin_nearest` shares the ``how`` argument with :func:`sjoin` but does not support the ``"inner"`` option.
:func:`sjoin_nearest` includes two additional arguments: ``max_distance`` and ``distance_col``.
:func:`sjoin_nearest` shares the ``how`` argument with :func:`sjoin`, and
includes two additional arguments: ``max_distance`` and ``distance_col``.
**max_distance**
+8 -9
View File
@@ -127,9 +127,7 @@ stria AUT 416600.0
return joined
def _basic_checks(
left_df, right_df, how, lsuffix, rsuffix, allowed_hows=("left", "right", "inner")
):
def _basic_checks(left_df, right_df, how, lsuffix, rsuffix):
"""Checks the validity of join input parameters.
`how` must be one of the valid options.
@@ -140,7 +138,7 @@ def _basic_checks(
------------
left_df : GeoDataFrame
right_df : GeoData Frame
how : str, one of allowed_hows
how : str, one of 'left', 'right', 'inner'
join type
lsuffix : str
left index suffix
@@ -157,6 +155,7 @@ def _basic_checks(
"'right_df' should be GeoDataFrame, got {}".format(type(right_df))
)
allowed_hows = ["left", "right", "inner"]
if how not in allowed_hows:
raise ValueError(
'`how` was "{}" but is expected to be in {}'.format(how, allowed_hows)
@@ -403,7 +402,7 @@ def _nearest_query(
def sjoin_nearest(
left_df: GeoDataFrame,
right_df: GeoDataFrame,
how: str = "left",
how: str = "inner",
max_distance: Optional[float] = None,
lsuffix: str = "left",
rsuffix: str = "right",
@@ -422,11 +421,13 @@ def sjoin_nearest(
Parameters
----------
left_df, right_df : GeoDataFrames
how : string, default 'left'
how : string, default 'inner'
The type of join:
* 'left': use keys from left_df; retain only left_df geometry column
* 'right': use keys from right_df; retain only right_df geometry column
* 'inner': use intersection of keys from both dfs; retain only
left_df geometry column
max_distance : float, default None
Maximum distance within which to query for nearest geometry.
Must be greater than 0.
@@ -503,9 +504,7 @@ countries_w_city_data[countries_w_city_data["name_left"] == "Italy"]
Every operation in GeoPandas is planar, i.e. the potential third
dimension is not taken into account.
"""
_basic_checks(
left_df, right_df, how, lsuffix, rsuffix, allowed_hows=("left", "right")
)
_basic_checks(left_df, right_df, how, lsuffix, rsuffix)
left_df.geometry.values.check_geographic_crs(stacklevel=1)
right_df.geometry.values.check_geographic_crs(stacklevel=1)
+57 -19
View File
@@ -585,13 +585,15 @@ def test_no_nearest_all():
),
)
class TestNearest:
@pytest.mark.parametrize("how_kwargs", ({}, {"how": "left"}, {"how": "right"}))
@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", ("inner", "abcde"))
@pytest.mark.parametrize("how", ("outer", "abcde"))
def test_invalid_hows(self, how: str):
left = geopandas.GeoDataFrame({"geometry": []})
right = geopandas.GeoDataFrame({"geometry": []})
@@ -629,16 +631,12 @@ class TestNearest:
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_how_left(self, distance_col: str):
def test_empty_left_df_how_left(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="left",
distance_col=distance_col,
)
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
@@ -658,7 +656,8 @@ class TestNearest:
if distance_col is not None:
assert joined[distance_col].isna().all()
def test_empty_join_due_to_max_distance_how_left(self):
@pytest.mark.parametrize("how", ["inner", "left"])
def test_empty_join_due_to_max_distance_how_left(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)]})
@@ -666,13 +665,16 @@ class TestNearest:
joined = sjoin_nearest(
left,
right,
how="left",
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):
@@ -693,19 +695,23 @@ class TestNearest:
expected = expected[["index_left", "geometry", "distances"]]
assert_geodataframe_equal(joined, expected)
def test_max_distance_how_left(self):
@pytest.mark.parametrize("how", ["inner", "left"])
def test_max_distance_how_left(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="left",
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):
@@ -724,6 +730,7 @@ class TestNearest:
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",
[
@@ -792,18 +799,21 @@ class TestNearest:
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]
expected_gdf = left.iloc[expected_left].copy()
expected_gdf["index_right"] = expected_right
# without distance col
joined = sjoin_nearest(left, right, how="left")
assert_geodataframe_equal(expected_gdf, joined)
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="left", distance_col="distance_col")
assert_geodataframe_equal(expected_gdf, joined)
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",
@@ -870,7 +880,7 @@ class TestNearest:
):
left = geopandas.GeoDataFrame({"geometry": geo_left})
right = geopandas.GeoDataFrame({"geometry": geo_right})
expected_gdf = right.iloc[expected_right]
expected_gdf = right.iloc[expected_right].copy()
expected_gdf["index_left"] = expected_left
expected_gdf = expected_gdf[["index_left", "geometry"]]
# without distance col
@@ -880,3 +890,31 @@ class TestNearest:
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)