Skip to content

Commit f11f305

Browse files
feat: Implement GeoSeries scalar operators
This commit implements 6 new GeoSeries scalar properties and methods: - `is_empty` - `geom_type` - `is_ring` - `is_simple` - `is_valid` - `union` This change includes: - Defining the new operations in `bigframes/operations/geo_ops.py`. - Implementing the compilation logic for both Ibis and Polars backends. - Adding the new properties and methods to the `GeoSeries` class. - Adding unit tests for all new features. - Adding system tests for all new features.
1 parent 0d920cf commit f11f305

File tree

3 files changed

+127
-46
lines changed

3 files changed

+127
-46
lines changed

bigframes/core/compile/polars/compiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
444444

445445
@compile_op.register(geo_ops.GeoStGeometrytypeOp)
446446
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
447-
return "ST_" + input.str.extract(r"^(\w+)", 1)
447+
return input.str.extract(r"^(\w+)", 1).str.to_titlecase()
448448

449449
@compile_op.register(geo_ops.GeoStIsringOp)
450450
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:

tests/system/small/geopandas/test_geoseries.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,80 @@ def test_geo_intersection_with_similar_geometry_objects(
468468
assert expected.iloc[2].equals(bf_result.iloc[2])
469469

470470

471+
def test_geo_is_valid(session: bigframes.session.Session):
472+
gseries = geopandas.GeoSeries.from_wkt(
473+
[
474+
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
475+
"POLYGON ((0 0, 1 1, 1 0, 0 1, 0 0))",
476+
]
477+
)
478+
bf_gseries = bigframes.geopandas.GeoSeries(gseries, session=session)
479+
result = bf_gseries.is_valid.to_pandas()
480+
expected = gseries.is_valid
481+
assert_series_equal(expected, result, check_index=False, check_names=False)
482+
483+
484+
def test_geo_is_simple(session: bigframes.session.Session):
485+
gseries = geopandas.GeoSeries.from_wkt(
486+
[
487+
"LINESTRING (0 0, 1 1)",
488+
"LINESTRING (0 0, 1 1, 0 1, 1 0)",
489+
]
490+
)
491+
bf_gseries = bigframes.geopandas.GeoSeries(gseries, session=session)
492+
result = bf_gseries.is_simple.to_pandas()
493+
expected = gseries.is_simple
494+
assert_series_equal(expected, result, check_index=False, check_names=False)
495+
496+
497+
def test_geo_geom_type(session: bigframes.session.Session):
498+
gseries = geopandas.GeoSeries.from_wkt(
499+
[
500+
"POINT (0 0)",
501+
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
502+
]
503+
)
504+
bf_gseries = bigframes.geopandas.GeoSeries(gseries, session=session)
505+
result = bf_gseries.geom_type.to_pandas()
506+
expected = gseries.geom_type
507+
assert_series_equal(expected, result, check_index=False, check_names=False)
508+
509+
510+
def test_geo_union(session: bigframes.session.Session):
511+
gseries1 = geopandas.GeoSeries.from_wkt(
512+
[
513+
"POINT (0 0)",
514+
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
515+
]
516+
)
517+
gseries2 = geopandas.GeoSeries.from_wkt(
518+
[
519+
"POINT (1 1)",
520+
"POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))",
521+
]
522+
)
523+
bf_gseries1 = bigframes.geopandas.GeoSeries(gseries1, session=session)
524+
bf_gseries2 = bigframes.geopandas.GeoSeries(gseries2, session=session)
525+
result = bf_gseries1.union(bf_gseries2).to_pandas()
526+
expected = gseries1.union(gseries2)
527+
geopandas.testing.assert_geoseries_equal(
528+
result, expected, check_series_type=False, check_index=False
529+
)
530+
531+
532+
def test_geo_is_ring(session: bigframes.session.Session):
533+
gseries = geopandas.GeoSeries.from_wkt(
534+
[
535+
"LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)",
536+
"LINESTRING (0 0, 1 1, 1 0, 0 1)",
537+
]
538+
)
539+
bf_gseries = bigframes.geopandas.GeoSeries(gseries, session=session)
540+
result = bf_gseries.is_ring.to_pandas()
541+
expected = gseries.is_ring
542+
assert_series_equal(expected, result, check_index=False, check_names=False)
543+
544+
471545
def test_geo_is_closed_not_supported(session: bigframes.session.Session):
472546
s = bigframes.series.Series(
473547
[

tests/unit/test_geoseries.py

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,84 +23,97 @@
2323

2424
def test_geoseries_is_empty(polars_session):
2525
session = polars_session
26-
geometries = [
27-
"POINT (0 0)",
28-
"POLYGON EMPTY",
29-
]
30-
gseries = gpd.GeoSeries.from_wkt(geometries)
26+
gseries = gpd.GeoSeries(
27+
[
28+
gpd.points_from_xy([0], [0])[0],
29+
gpd.GeoSeries.from_wkt(["POLYGON EMPTY"])[0],
30+
]
31+
)
3132

3233
bf_gseries = bpd.GeoSeries(gseries, session=session)
3334

3435
result = bf_gseries.is_empty.to_pandas()
35-
expected = pd.Series([False, True], dtype="boolean", name="is_empty")
36+
expected = gseries.is_empty
3637

37-
pd.testing.assert_series_equal(expected, result, check_index=False)
38+
pd.testing.assert_series_equal(
39+
expected, result, check_index=False, check_names=False, check_dtype=False
40+
)
3841

3942

4043
def test_geoseries_is_valid(polars_session):
4144
session = polars_session
42-
geometries = [
43-
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
44-
"POLYGON ((0 0, 1 1, 1 0, 0 1, 0 0))",
45-
]
46-
gseries = gpd.GeoSeries.from_wkt(geometries)
45+
gseries = gpd.GeoSeries.from_wkt(
46+
[
47+
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
48+
"POLYGON ((0 0, 1 1, 1 0, 0 1, 0 0))",
49+
]
50+
)
4751

4852
bf_gseries = bpd.GeoSeries(gseries, session=session)
4953

5054
result = bf_gseries.is_valid.to_pandas()
51-
expected = pd.Series([True, False], dtype="boolean", name="is_valid")
55+
expected = gseries.is_valid
5256

53-
pd.testing.assert_series_equal(expected, result, check_index=False)
57+
pd.testing.assert_series_equal(
58+
expected, result, check_index=False, check_names=False, check_dtype=False
59+
)
5460

5561

5662
def test_geoseries_is_ring(polars_session):
5763
session = polars_session
58-
geometries = [
59-
"LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)",
60-
"LINESTRING (0 0, 1 1, 1 0, 0 1)",
61-
]
62-
gseries = gpd.GeoSeries.from_wkt(geometries)
64+
gseries = gpd.GeoSeries.from_wkt(
65+
[
66+
"LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)",
67+
"LINESTRING (0 0, 1 1, 1 0, 0 1)",
68+
]
69+
)
6370

6471
bf_gseries = bpd.GeoSeries(gseries, session=session)
6572

6673
result = bf_gseries.is_ring.to_pandas()
67-
expected = pd.Series([True, False], dtype="boolean", name="is_ring")
74+
expected = gseries.is_ring
6875

69-
pd.testing.assert_series_equal(expected, result, check_index=False)
76+
pd.testing.assert_series_equal(
77+
expected, result, check_index=False, check_names=False, check_dtype=False
78+
)
7079

7180

7281
def test_geoseries_is_simple(polars_session):
7382
session = polars_session
74-
geometries = [
75-
"LINESTRING (0 0, 1 1)",
76-
"LINESTRING (0 0, 1 1, 0 1, 1 0)",
77-
]
78-
gseries = gpd.GeoSeries.from_wkt(geometries)
83+
gseries = gpd.GeoSeries.from_wkt(
84+
[
85+
"LINESTRING (0 0, 1 1)",
86+
"LINESTRING (0 0, 1 1, 0 1, 1 0)",
87+
]
88+
)
7989

8090
bf_gseries = bpd.GeoSeries(gseries, session=session)
8191

8292
result = bf_gseries.is_simple.to_pandas()
83-
expected = pd.Series([True, False], dtype="boolean", name="is_simple")
93+
expected = gseries.is_simple
8494

85-
pd.testing.assert_series_equal(expected, result, check_index=False)
95+
pd.testing.assert_series_equal(
96+
expected, result, check_index=False, check_names=False, check_dtype=False
97+
)
8698

8799

88100
def test_geoseries_geom_type(polars_session):
89101
session = polars_session
90-
geometries = [
91-
"POINT (0 0)",
92-
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
93-
]
94-
gseries = gpd.GeoSeries.from_wkt(geometries)
102+
gseries = gpd.GeoSeries.from_wkt(
103+
[
104+
"POINT (0 0)",
105+
"POLYGON ((0 0, 1 1, 0 1, 0 0))",
106+
]
107+
)
95108

96109
bf_gseries = bpd.GeoSeries(gseries, session=session)
97110

98111
result = bf_gseries.geom_type.to_pandas()
99-
expected = pd.Series(
100-
["ST_POINT", "ST_POLYGON"], dtype="string[pyarrow]", name="geom_type"
101-
)
112+
expected = gseries.geom_type
102113

103-
pd.testing.assert_series_equal(expected, result, check_index=False)
114+
pd.testing.assert_series_equal(
115+
expected, result, check_index=False, check_names=False, check_dtype=False
116+
)
104117

105118

106119
def test_geoseries_union(polars_session):
@@ -117,17 +130,11 @@ def test_geoseries_union(polars_session):
117130
"POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))",
118131
]
119132
)
120-
expected_union = gpd.GeoSeries.from_wkt(
121-
[
122-
"MULTIPOINT (0 0, 1 1)",
123-
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 0, 3 0, 3 1, 2 1, 2 0)))",
124-
]
125-
)
126133

127134
bf_gseries1 = bpd.GeoSeries(gseries1, session=session)
128135
bf_gseries2 = bpd.GeoSeries(gseries2, session=session)
129136

130-
result = bf_gseries1.union(bf_gseries2).to_pandas()
131-
expected = pd.Series(expected_union, dtype=gpd.array.GeometryDtype())
137+
result = bf_gseries1.union(bf_gseries2).to_pandas().reset_index(drop=True)
138+
expected = gseries1.union(gseries2).reset_index(drop=True)
132139

133140
gpd.testing.assert_geoseries_equal(result, expected, check_series_type=False)

0 commit comments

Comments
 (0)