Skip to content

Commit 8b5ffa8

Browse files
arwas11tswast
andauthored
feat: add bigframes.bigquery.st_area and suggest it from GeoSeries.area (#1318)
* feat: Add GeoSeries.area * fix formatting * fix import error * fix format and return type * update test * add type ignore flag * add and test st_area * update goseries notebook with area and st_area * ignore mypy error * update the notebook * update the notebook * ignore exception in notebook * update test data and add comment * Update tests/system/small/bigquery/test_geo.py * Update tests/system/small/bigquery/test_geo.py --------- Co-authored-by: Tim Sweña (Swast) <[email protected]>
1 parent 9a21f25 commit 8b5ffa8

File tree

10 files changed

+410
-41
lines changed

10 files changed

+410
-41
lines changed

bigframes/bigquery/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
unix_millis,
2828
unix_seconds,
2929
)
30+
from bigframes.bigquery._operations.geo import st_area
3031
from bigframes.bigquery._operations.json import (
3132
json_extract,
3233
json_extract_array,
@@ -45,6 +46,8 @@
4546
"array_length",
4647
"array_agg",
4748
"array_to_string",
49+
# geo ops
50+
"st_area",
4851
# json ops
4952
"json_set",
5053
"json_extract",

bigframes/bigquery/_operations/geo.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from bigframes import operations as ops
18+
import bigframes.geopandas
19+
import bigframes.series
20+
21+
"""
22+
Search functions defined from
23+
https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions
24+
"""
25+
26+
27+
def st_area(self) -> bigframes.series.Series:
28+
"""
29+
Returns the area in square meters covered by the polygons in the input
30+
GEOGRAPHY.
31+
32+
If geography_expression is a point or a line, returns zero. If
33+
geography_expression is a collection, returns the area of the polygons
34+
in the collection; if the collection doesn't contain polygons, returns zero.
35+
36+
37+
..note::
38+
BigQuery's Geography functions, like `st_area`, interpet the geomtry
39+
data type as a point set on the Earth's surface. A point set is a set
40+
of points, lines, and polygons on the WGS84 reference spheroid, with
41+
geodesic edges. See: https://cloud.google.com/bigquery/docs/geospatial-data
42+
43+
44+
**Examples:**
45+
46+
>>> import bigframes.geopandas
47+
>>> import bigframes.pandas as bpd
48+
>>> import bigframes.bigquery as bbq
49+
>>> from shapely.geometry import Polygon, LineString, Point
50+
>>> bpd.options.display.progress_bar = None
51+
52+
>>> series = bigframes.geopandas.GeoSeries(
53+
... [
54+
... Polygon([(0.0, 0.0), (0.1, 0.1), (0.0, 0.1)]),
55+
... Polygon([(0.10, 0.4), (0.9, 0.5), (0.10, 0.5)]),
56+
... Polygon([(0.1, 0.1), (0.2, 0.1), (0.2, 0.2)]),
57+
... LineString([(0, 0), (1, 1), (0, 1)]),
58+
... Point(0, 1),
59+
... ]
60+
... )
61+
>>> series
62+
0 POLYGON ((0 0, 0.1 0.1, 0 0.1, 0 0))
63+
1 POLYGON ((0.1 0.4, 0.9 0.5, 0.1 0.5, 0.1 0.4))
64+
2 POLYGON ((0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))
65+
3 LINESTRING (0 0, 1 1, 0 1)
66+
4 POINT (0 1)
67+
dtype: geometry
68+
69+
>>> bbq.st_area(series)
70+
0 61821689.855985
71+
1 494563347.88721
72+
2 61821689.855841
73+
3 0.0
74+
4 0.0
75+
dtype: Float64
76+
77+
Use `round()` to round the outputed areas to the neares ten millions
78+
79+
>>> bbq.st_area(series).round(-7)
80+
0 60000000.0
81+
1 490000000.0
82+
2 60000000.0
83+
3 0.0
84+
4 0.0
85+
dtype: Float64
86+
87+
Returns:
88+
bigframes.pandas.Series:
89+
Series of float representing the areas.
90+
"""
91+
series = self._apply_unary_op(ops.geo_area_op)
92+
series.name = None
93+
return series

bigframes/core/compile/scalar_op_compiler.py

+5
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,11 @@ def geo_y_op_impl(x: ibis_types.Value):
993993
return typing.cast(ibis_types.GeoSpatialValue, x).y()
994994

995995

996+
@scalar_op_compiler.register_unary_op(ops.geo_area_op)
997+
def geo_area_op_impl(x: ibis_types.Value):
998+
return typing.cast(ibis_types.GeoSpatialValue, x).area()
999+
1000+
9961001
# Parameterized ops
9971002
@scalar_op_compiler.register_unary_op(ops.StructFieldOp, pass_op=True)
9981003
def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp):

bigframes/geopandas/geoseries.py

+27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16+
import bigframes_vendored.constants as constants
1617
import bigframes_vendored.geopandas.geoseries as vendored_geoseries
1718
import geopandas.array # type: ignore
1819

@@ -39,3 +40,29 @@ def y(self) -> bigframes.series.Series:
3940
series = self._apply_unary_op(ops.geo_y_op)
4041
series.name = None
4142
return series
43+
44+
# GeoSeries.area overrides Series.area with something totally different.
45+
# Ignore this type error, as we are trying to be as close to geopandas as
46+
# we can.
47+
@property
48+
def area(self, crs=None) -> bigframes.series.Series: # type: ignore
49+
"""Returns a Series containing the area of each geometry in the GeoSeries
50+
expressed in the units of the CRS.
51+
52+
Args:
53+
crs (optional):
54+
Coordinate Reference System of the geometry objects. Can be
55+
anything accepted by pyproj.CRS.from_user_input(), such as an
56+
authority string (eg “EPSG:4326”) or a WKT string.
57+
58+
Returns:
59+
bigframes.pandas.Series:
60+
Series of float representing the areas.
61+
62+
Raises:
63+
NotImplementedError:
64+
GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), insetead.
65+
"""
66+
raise NotImplementedError(
67+
f"GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. {constants.FEEDBACK_LINK}"
68+
)

bigframes/operations/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
SqlScalarOp,
8585
where_op,
8686
)
87-
from bigframes.operations.geo_ops import geo_x_op, geo_y_op
87+
from bigframes.operations.geo_ops import geo_area_op, geo_x_op, geo_y_op
8888
from bigframes.operations.json_ops import (
8989
JSONExtract,
9090
JSONExtractArray,
@@ -332,6 +332,7 @@
332332
# Geo ops
333333
"geo_x_op",
334334
"geo_y_op",
335+
"geo_area_op",
335336
# Numpy ops mapping
336337
"NUMPY_TO_BINOP",
337338
"NUMPY_TO_OP",

bigframes/operations/geo_ops.py

+7
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@
2929
dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like"
3030
),
3131
)
32+
33+
geo_area_op = base_ops.create_unary_op(
34+
name="geo_area",
35+
type_signature=op_typing.FixedOutputType(
36+
dtypes.is_geo_like, dtypes.FLOAT_DTYPE, description="geo-like"
37+
),
38+
)

0 commit comments

Comments
 (0)