ENH: add a keyword to control alignment in binary operations and predicates (#1668)

* align keyword

* tests

* first set of docstrings

* docstrings

* docs

* shapely test

* fix condition

* fix project test

* docstring changes per review
This commit is contained in:
Martin Fleischmann
2021-02-18 21:51:48 +00:00
committed by GitHub
parent 0ae468fcad
commit 53e06302cb
12 changed files with 1934 additions and 81 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ examples/nybb_*.zip
doc/source/savefig
doc/source/reference
doc/source/docs/reference/api
doc/source/docs/reference/api/*.rst
geopandas.egg-info
geopandas/version.py
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105.44mm" height="35.61mm" viewBox="0 0 298.88 100.95"><line x1="120.15" y1="48.9" x2="162.49" y2="48.9" style="fill:none;stroke:#1d1d1b;stroke-miterlimit:10"/><polygon points="161.03 53.89 169.67 48.9 161.03 43.91 161.03 53.89" style="fill:#1d1d1b"/><circle cx="45.9" cy="48.9" r="34.98" style="fill:#e21c84;stroke:#fff;stroke-miterlimit:10;opacity:0.4"/><circle cx="80.88" cy="48.9" r="34.98" style="fill:#0f9c5a;stroke:#fff;stroke-miterlimit:10;opacity:0.6"/><g style="opacity:0.8"><circle cx="214.68" cy="48.9" r="34.98" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><g style="opacity:0.8"><circle cx="249.65" cy="48.9" r="34.98" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><path d="M214.68,48.9a35,35,0,0,1,17.48-30.29,35,35,0,1,0,0,60.57A34.93,34.93,0,0,1,214.68,48.9Z" style="fill:#fec905;opacity:0.8"/></svg>

After

Width:  |  Height:  |  Size: 1001 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105.44mm" height="35.61mm" viewBox="0 0 298.88 100.95"><g style="opacity:0.8"><circle cx="214.68" cy="51.28" r="34.98" transform="matrix(0.94, -0.33, 0.33, 0.94, -4.87, 73.95)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><g style="opacity:0.8"><circle cx="249.65" cy="51.28" r="34.98" transform="translate(74.78 236.69) rotate(-58.28)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><line x1="120.15" y1="51.28" x2="162.49" y2="51.28" style="fill:none;stroke:#1d1d1b;stroke-miterlimit:10"/><polygon points="161.03 56.27 169.67 51.28 161.03 46.3 161.03 56.27" style="fill:#1d1d1b"/><circle cx="45.9" cy="51.28" r="34.98" style="fill:#e21c84;stroke:#fff;stroke-miterlimit:10;opacity:0.4"/><circle cx="80.88" cy="51.28" r="34.98" style="fill:#0f9c5a;stroke:#fff;stroke-miterlimit:10;opacity:0.6"/><ellipse cx="232.16" cy="51.28" rx="17.49" ry="30.29" style="fill:#fec905;opacity:0.8"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105.44mm" height="35.61mm" viewBox="0 0 298.88 100.95"><g style="opacity:0.8"><circle cx="214.68" cy="51.28" r="34.98" transform="matrix(0.94, -0.33, 0.33, 0.94, -4.87, 73.95)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><g style="opacity:0.8"><circle cx="249.65" cy="51.28" r="34.98" transform="translate(74.78 236.69) rotate(-58.28)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><line x1="120.15" y1="51.28" x2="162.49" y2="51.28" style="fill:none;stroke:#1d1d1b;stroke-miterlimit:10"/><polygon points="161.03 56.27 169.67 51.28 161.03 46.3 161.03 56.27" style="fill:#1d1d1b"/><circle cx="45.9" cy="51.28" r="34.98" style="fill:#e21c84;stroke:#fff;stroke-miterlimit:10;opacity:0.4"/><circle cx="80.88" cy="51.28" r="34.98" style="fill:#0f9c5a;stroke:#fff;stroke-miterlimit:10;opacity:0.6"/><g style="opacity:0.8"><path d="M214.68,51.28A35,35,0,0,1,232.16,21a35,35,0,1,0,0,60.57A35,35,0,0,1,214.68,51.28Z" style="fill:#fec905"/><path d="M249.65,16.31A34.81,34.81,0,0,0,232.16,21a35,35,0,0,1,0,60.57,35,35,0,1,0,17.49-65.26Z" style="fill:#fec905"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105.44mm" height="35.61mm" viewBox="0 0 298.88 100.95"><g style="opacity:0.8"><circle cx="214.68" cy="50.49" r="34.98" transform="matrix(0.94, -0.33, 0.33, 0.94, -4.61, 73.91)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><g style="opacity:0.8"><circle cx="249.65" cy="50.49" r="34.98" transform="translate(75.45 236.31) rotate(-58.28)" style="fill:none;stroke:#fec905;stroke-miterlimit:10;stroke-dasharray:1.9972946643829346,1.9972946643829346"/></g><line x1="120.15" y1="50.49" x2="162.49" y2="50.49" style="fill:none;stroke:#1d1d1b;stroke-miterlimit:10"/><polygon points="161.03 55.48 169.67 50.49 161.03 45.5 161.03 55.48" style="fill:#1d1d1b"/><circle cx="45.9" cy="50.49" r="34.98" style="fill:#e21c84;stroke:#fff;stroke-miterlimit:10;opacity:0.4"/><circle cx="80.88" cy="50.49" r="34.98" style="fill:#0f9c5a;stroke:#fff;stroke-miterlimit:10;opacity:0.6"/><path d="M249.65,15.51a34.91,34.91,0,0,0-17.49,4.69,35,35,0,1,0,0,60.57,35,35,0,1,0,17.49-65.26Z" style="fill:#fec905;opacity:0.8"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

+2 -1
View File
@@ -49,11 +49,12 @@ Binary Predicates
.. autosummary::
:toctree: api/
GeoSeries.geom_almost_equals
GeoSeries.contains
GeoSeries.crosses
GeoSeries.disjoint
GeoSeries.geom_equals
GeoSeries.geom_almost_equals
GeoSeries.geom_equals_exact
GeoSeries.intersects
GeoSeries.overlaps
GeoSeries.touches
+1794 -78
View File
File diff suppressed because it is too large Load Diff
+111
View File
@@ -81,6 +81,7 @@ class TestGeomMethods:
self.empty = GeoSeries([])
self.all_none = GeoSeries([None, None])
self.empty_poly = Polygon()
self.g9 = GeoSeries(self.g0, index=range(1, 8))
# Crossed lines
self.l3 = LineString([(0, 0), (1, 1)])
@@ -244,15 +245,24 @@ class TestGeomMethods:
"intersection", self.all_none, self.g1, self.empty
)
assert len(self.g0.intersection(self.g9, align=True) == 8)
assert len(self.g0.intersection(self.g9, align=False) == 7)
def test_union_series(self):
self._test_binary_topological("union", self.sq, self.g1, self.g2)
assert len(self.g0.union(self.g9, align=True) == 8)
assert len(self.g0.union(self.g9, align=False) == 7)
def test_union_polygon(self):
self._test_binary_topological("union", self.sq, self.g1, self.t2)
def test_symmetric_difference_series(self):
self._test_binary_topological("symmetric_difference", self.sq, self.g3, self.g4)
assert len(self.g0.symmetric_difference(self.g9, align=True) == 8)
assert len(self.g0.symmetric_difference(self.g9, align=False) == 7)
def test_symmetric_difference_poly(self):
expected = GeoSeries([GeometryCollection(), self.sq], crs=self.g3.crs)
self._test_binary_topological(
@@ -263,6 +273,9 @@ class TestGeomMethods:
expected = GeoSeries([GeometryCollection(), self.t2])
self._test_binary_topological("difference", expected, self.g1, self.g2)
assert len(self.g0.difference(self.g9, align=True) == 8)
assert len(self.g0.difference(self.g9, align=False) == 7)
def test_difference_poly(self):
expected = GeoSeries([self.t1, self.t1])
self._test_binary_topological("difference", expected, self.g1, self.t2)
@@ -341,6 +354,12 @@ class TestGeomMethods:
expected = [True, False, True, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.contains(self.t1))
expected = [False, True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=True))
expected = [False, False, True, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=False))
def test_length(self):
expected = Series(np.array([2 + np.sqrt(2), 4]), index=self.g1.index)
self._test_unary_real("length", expected, self.g1)
@@ -359,10 +378,22 @@ class TestGeomMethods:
expected = [False, True]
assert_array_dtype_equal(expected, self.crossed_lines.crosses(self.l3))
expected = [False] * 8
assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=True))
expected = [False] * 7
assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=False))
def test_disjoint(self):
expected = [False, False, False, False, False, True, False]
assert_array_dtype_equal(expected, self.g0.disjoint(self.t1))
expected = [False] * 8
assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=True))
expected = [False, False, False, False, True, False, False]
assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=False))
def test_relate(self):
expected = Series(
[
@@ -381,6 +412,36 @@ class TestGeomMethods:
expected = Series(["FF0FFF212", None], index=self.g6.index)
assert_array_dtype_equal(expected, self.g6.relate(self.na_none))
expected = Series(
[
None,
"2FFF1FFF2",
"2FFF1FFF2",
"2FFF1FFF2",
"2FFF1FFF2",
"0FFFFFFF2",
None,
None,
],
index=range(8),
)
assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=True))
expected = Series(
[
"FF2F11212",
"2FF11F212",
"212FF1FF2",
"FF2F1F212",
"FF2FF10F2",
None,
None,
],
index=self.g0.index,
)
assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=False))
def test_distance(self):
expected = Series(
np.array([np.sqrt((5 - 1) ** 2 + (5 - 1) ** 2), np.nan]), self.na_none.index
@@ -390,6 +451,13 @@ class TestGeomMethods:
expected = Series(np.array([np.sqrt(4 ** 2 + 4 ** 2), np.nan]), self.g6.index)
assert_array_dtype_equal(expected, self.g6.distance(self.na_none))
expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=True))
val = self.g0.iloc[4].distance(self.g9.iloc[4])
expected = Series(np.array([0, 0, 0, 0, val, np.nan, np.nan]), self.g0.index)
assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=False))
def test_distance_crs_warning(self):
with pytest.warns(UserWarning, match="Geometry is in a geographic CRS"):
self.g4.distance(self.p0)
@@ -410,6 +478,12 @@ class TestGeomMethods:
expected = [False] * 7
assert_array_dtype_equal(expected, self.g0.intersects(self.empty_poly))
expected = [False, True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=True))
expected = [True, True, True, True, False, False, False]
assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=False))
def test_overlaps(self):
expected = [True, True, False, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.overlaps(self.inner_sq))
@@ -417,10 +491,22 @@ class TestGeomMethods:
expected = [False, False]
assert_array_dtype_equal(expected, self.g4.overlaps(self.t1))
expected = [False] * 8
assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=True))
expected = [False] * 7
assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=False))
def test_touches(self):
expected = [False, True, False, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.touches(self.t1))
expected = [False] * 8
assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=True))
expected = [True, False, False, True, False, False, False]
assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=False))
def test_within(self):
expected = [True, False, False, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.within(self.t1))
@@ -428,6 +514,12 @@ class TestGeomMethods:
expected = [True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.within(self.sq))
expected = [False, True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.within(self.g9, align=True))
expected = [False, True, False, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.within(self.g9, align=False))
def test_covers_itself(self):
# Each polygon in a Series covers itself
res = self.g1.covers(self.g1)
@@ -439,6 +531,12 @@ class TestGeomMethods:
exp = Series([True, False])
assert_series_equal(res, exp)
expected = [False, True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=True))
expected = [False, False, True, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=False))
def test_covers_inverse(self):
res = self.g8.covers(self.g7)
exp = Series([False, False])
@@ -453,6 +551,12 @@ class TestGeomMethods:
exp = Series([True, True])
assert_series_equal(res, exp)
expected = [False, True, True, True, True, True, False, False]
assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=True))
expected = [False, True, False, False, False, False, False]
assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=False))
def test_is_valid(self):
expected = Series(np.array([True] * len(self.g1)), self.g1.index)
self._test_unary_real("is_valid", expected, self.g1)
@@ -572,6 +676,13 @@ class TestGeomMethods:
expected = Series([1.0, 0.5], index=self.g5.index)
self._test_binary_real("project", expected, self.g5, p, normalized=True)
s = GeoSeries([Point(2, 2), Point(0.5, 0.5)], index=[1, 2])
expected = Series([np.nan, 2.0, np.nan])
assert_series_equal(self.g5.project(s), expected)
expected = Series([2.0, 0.5], index=self.g5.index)
assert_series_equal(self.g5.project(s, align=False), expected)
def test_affine_transform(self):
# 45 degree reflection matrix
matrix = [0, 1, 1, 0, 0, 0]
+19 -1
View File
@@ -136,20 +136,38 @@ class TestSeries:
def test_geom_equals_align(self):
with pytest.warns(UserWarning, match="The indices .+ different"):
a = self.a1.geom_equals(self.a2)
a = self.a1.geom_equals(self.a2, align=True)
exp = pd.Series([False, True, False], index=["A", "B", "C"])
assert_series_equal(a, exp)
a = self.a1.geom_equals(self.a2, align=False)
exp = pd.Series([False, False], index=["A", "B"])
assert_series_equal(a, exp)
def test_geom_almost_equals(self):
# TODO: test decimal parameter
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_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]
)
assert_array_equal(
self.a1.geom_equals_exact(self.a2, 0.001, align=False), [False, False]
)
def test_equal_comp_op(self):
s = GeoSeries([Point(x, x) for x in range(3)])
res = s == Point(1, 1)