From 28afa2c73c0517f9365fab05193706631b656551 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Tue, 22 Apr 2025 11:05:43 -0700 Subject: [PATCH 01/28] fix: DataFrame accessors is not pupulated (#1639) --- docs/reference/bigframes.pandas/frame.rst | 2 +- docs/reference/bigframes.pandas/series.rst | 10 +++++----- docs/templates/toc.yml | 4 ++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/reference/bigframes.pandas/frame.rst b/docs/reference/bigframes.pandas/frame.rst index 4e231bd821..ea4c6dec1c 100644 --- a/docs/reference/bigframes.pandas/frame.rst +++ b/docs/reference/bigframes.pandas/frame.rst @@ -22,7 +22,7 @@ Accessors Plotting handling ^^^^^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.plotting +.. autoclass:: bigframes.operations.plotting.PlotAccessor :members: :inherited-members: :undoc-members: diff --git a/docs/reference/bigframes.pandas/series.rst b/docs/reference/bigframes.pandas/series.rst index 3cfb7d89dd..41b1529b0c 100644 --- a/docs/reference/bigframes.pandas/series.rst +++ b/docs/reference/bigframes.pandas/series.rst @@ -22,7 +22,7 @@ Accessors Datetime properties ^^^^^^^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.datetimes +.. autoclass:: bigframes.operations.datetimes.DatetimeMethods :members: :inherited-members: :undoc-members: @@ -30,7 +30,7 @@ Datetime properties String handling ^^^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.strings +.. autoclass:: bigframes.operations.strings.StringMethods :members: :inherited-members: :undoc-members: @@ -38,7 +38,7 @@ String handling List handling ^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.lists +.. autoclass:: bigframes.operations.lists.ListAccessor :members: :inherited-members: :undoc-members: @@ -54,7 +54,7 @@ Struct handling Blob handling ^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.blob +.. autoclass:: bigframes.operations.blob.BlobAccessor :members: :inherited-members: :undoc-members: @@ -62,7 +62,7 @@ Blob handling Plotting handling ^^^^^^^^^^^^^^^^^ -.. automodule:: bigframes.operations.plotting +.. autoclass:: bigframes.operations.plotting.PlotAccessor :members: :inherited-members: :undoc-members: diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index 1e712848df..f70e81d196 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -47,6 +47,8 @@ uid: bigframes.operations.structs.StructFrameAccessor - name: AI uid: bigframes.operations.ai.AIAccessor + status: beta + name: DataFrame - items: - name: DataFrameGroupBy uid: bigframes.core.groupby.DataFrameGroupBy @@ -84,6 +86,8 @@ uid: bigframes.operations.strings.StringMethods - name: StructAccessor uid: bigframes.operations.structs.StructAccessor + - name: ListAccessor + uid: bigframes.operations.lists.ListAccessor - name: PlotAccessor uid: bigframes.operations.plotting.PlotAccessor - name: BlobAccessor From 48d10d1f0150a29dd3b91f505f8d3874e0b88c42 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 22 Apr 2025 11:58:38 -0700 Subject: [PATCH 02/28] docs: note that `udf` is in preview and must be python 3.11 compatible (#1629) * docs: note that `udf` is in preview and must be python 3.11 compatible * user stacklevel=5 to optimize the warning for bpd.udf entry point --- bigframes/functions/_function_session.py | 9 ++++++--- bigframes/session/__init__.py | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bigframes/functions/_function_session.py b/bigframes/functions/_function_session.py index 6d2f0dbd57..7fb5cc114b 100644 --- a/bigframes/functions/_function_session.py +++ b/bigframes/functions/_function_session.py @@ -740,9 +740,12 @@ def udf( BigQuery managed function. .. note:: - The udf must be self-contained, i.e. it must not contain any + This feature is in preview. The code in the udf must be + (1) self-contained, i.e. it must not contain any references to an import or variable defined outside the function - body. + body, and + (2) Python 3.11 compatible, as that is the environment + in which the code is executed in the cloud. .. note:: Please have following IAM roles enabled for you: @@ -801,7 +804,7 @@ def udf( https://pip.pypa.io/en/stable/reference/requirements-file-format/. """ - warnings.warn("udf is in preview.", category=bfe.PreviewWarning) + warnings.warn("udf is in preview.", category=bfe.PreviewWarning, stacklevel=5) # Some defaults may be used from the session if not provided otherwise. session = self._resolve_session(session) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7229a4641b..b189c0e194 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -1426,9 +1426,12 @@ def udf( [BigQuery managed user-defined function](https://cloud.google.com/bigquery/docs/user-defined-functions-python). .. note:: - The udf must be self-contained, i.e. it must not contain any + This feature is in preview. The code in the udf must be + (1) self-contained, i.e. it must not contain any references to an import or variable defined outside the function - body. + body, and + (2) Python 3.11 compatible, as that is the environment + in which the code is executed in the cloud. .. note:: Please have BigQuery Data Editor (roles/bigquery.dataEditor) IAM From 53fc25bfc86e166b91e5001506051b1cac34c996 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 22 Apr 2025 12:22:55 -0700 Subject: [PATCH 03/28] fix: Prefer remote schema instead of throwing on materialize conflicts (#1644) --- bigframes/session/executor.py | 15 +++++++++++---- tests/system/small/test_dataframe.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 4c27c25058..496ff560dd 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -16,6 +16,7 @@ import abc import dataclasses +import itertools from typing import Callable, Iterator, Literal, Mapping, Optional, Sequence, Union from google.cloud import bigquery @@ -37,10 +38,16 @@ def to_arrow_table(self) -> pyarrow.Table: # Need to provide schema if no result rows, as arrow can't infer # If ther are rows, it is safest to infer schema from batches. # Any discrepencies between predicted schema and actual schema will produce errors. - return pyarrow.Table.from_batches( - self.arrow_batches(), - self.schema.to_pyarrow() if not self.total_rows else None, - ) + batches = iter(self.arrow_batches()) + peek_it = itertools.islice(batches, 0, 1) + peek_value = list(peek_it) + # TODO: Enforce our internal schema on the table for consistency + if len(peek_value) > 0: + return pyarrow.Table.from_batches( + itertools.chain(peek_value, batches), # reconstruct + ) + else: + return self.schema.to_pyarrow().empty_table() class Executor(abc.ABC): diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index e77319b551..83901562d3 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -5283,6 +5283,16 @@ def test_to_gbq_and_create_dataset(session, scalars_df_index, dataset_id_not_cre assert not loaded_scalars_df_index.empty +def test_read_gbq_to_pandas_no_exec(unordered_session: bigframes.Session): + metrics = unordered_session._metrics + execs_pre = metrics.execution_count + df = unordered_session.read_gbq("bigquery-public-data.ml_datasets.penguins") + df.to_pandas() + execs_post = metrics.execution_count + assert df.shape == (344, 7) + assert execs_pre == execs_post + + def test_to_gbq_table_labels(scalars_df_index): destination_table = "bigframes-dev.bigframes_tests_sys.table_labels" result_table = scalars_df_index.to_gbq( From 102d363aa7e3245ff262c817bc756ea0eaee57e7 Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:59:05 -0500 Subject: [PATCH 04/28] feat: support passing list of values to bigframes.core.sql.simple_literal (#1641) --- bigframes/core/sql.py | 8 +++++- tests/unit/core/test_sql.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/bigframes/core/sql.py b/bigframes/core/sql.py index d197993305..04e678e713 100644 --- a/bigframes/core/sql.py +++ b/bigframes/core/sql.py @@ -43,7 +43,9 @@ ### Writing SQL Values (literals, column references, table references, etc.) -def simple_literal(value: bytes | str | int | bool | float | datetime.datetime | None): +def simple_literal( + value: bytes | str | int | bool | float | datetime.datetime | list | None, +): """Return quoted input string.""" # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals @@ -80,6 +82,10 @@ def simple_literal(value: bytes | str | int | bool | float | datetime.datetime | elif isinstance(value, decimal.Decimal): # TODO: disambiguate BIGNUMERIC based on scale and/or precision return f"CAST('{str(value)}' AS NUMERIC)" + elif isinstance(value, list): + simple_literals = [simple_literal(i) for i in value] + return f"[{', '.join(simple_literals)}]" + else: raise ValueError(f"Cannot produce literal for {value}") diff --git a/tests/unit/core/test_sql.py b/tests/unit/core/test_sql.py index 913a5b61fe..1b5c63e061 100644 --- a/tests/unit/core/test_sql.py +++ b/tests/unit/core/test_sql.py @@ -73,6 +73,63 @@ def test_simple_literal(value, expected_pattern): assert re.match(expected_pattern, got) is not None +@pytest.mark.parametrize( + ("value", "expected"), + ( + # Try to have some list of literals for each scalar data type: + # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types + ([None, None], "[NULL, NULL]"), + ([True, False], "[True, False]"), + ( + [b"\x01\x02\x03ABC", b"\x01\x02\x03ABC"], + "[b'\\x01\\x02\\x03ABC', b'\\x01\\x02\\x03ABC']", + ), + ( + [datetime.date(2025, 1, 1), datetime.date(2025, 1, 1)], + "[DATE('2025-01-01'), DATE('2025-01-01')]", + ), + ( + [datetime.datetime(2025, 1, 2, 3, 45, 6, 789123)], + "[DATETIME('2025-01-02T03:45:06.789123')]", + ), + ( + [shapely.Point(0, 1), shapely.Point(0, 2)], + "[ST_GEOGFROMTEXT('POINT (0 1)'), ST_GEOGFROMTEXT('POINT (0 2)')]", + ), + # TODO: INTERVAL type (e.g. from dateutil.relativedelta) + # TODO: JSON type (TBD what Python object that would correspond to) + ([123, 456], "[123, 456]"), + ( + [decimal.Decimal("123.75"), decimal.Decimal("456.78")], + "[CAST('123.75' AS NUMERIC), CAST('456.78' AS NUMERIC)]", + ), + # TODO: support BIGNUMERIC by looking at precision/scale of the DECIMAL + ([123.75, 456.78], "[123.75, 456.78]"), + # TODO: support RANGE type + (["abc", "def"], "['abc', 'def']"), + # TODO: support STRUCT type (possibly another method?) + ( + [datetime.time(12, 34, 56, 789123), datetime.time(11, 25, 56, 789123)], + "[TIME(DATETIME('1970-01-01 12:34:56.789123')), TIME(DATETIME('1970-01-01 11:25:56.789123'))]", + ), + ( + [ + datetime.datetime( + 2025, 1, 2, 3, 45, 6, 789123, tzinfo=datetime.timezone.utc + ), + datetime.datetime( + 2025, 2, 1, 4, 45, 6, 789123, tzinfo=datetime.timezone.utc + ), + ], + "[TIMESTAMP('2025-01-02T03:45:06.789123+00:00'), TIMESTAMP('2025-02-01T04:45:06.789123+00:00')]", + ), + ), +) +def test_simple_literal_w_list(value: list, expected: str): + got = sql.simple_literal(value) + assert got == expected + + def test_create_vector_search_sql_simple(): result_query = sql.create_vector_search_sql( sql_string="SELECT embedding FROM my_embeddings_table WHERE id = 1", From f67aa414bb2247e415710b2f5e8ea2f29de7b19e Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:20:43 -0700 Subject: [PATCH 05/28] chore: remove read_gbq for large tables test. (#1646) --- tests/system/large/test_session.py | 37 ------------------------------ 1 file changed, 37 deletions(-) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py index 90955f5ddf..d28146498d 100644 --- a/tests/system/large/test_session.py +++ b/tests/system/large/test_session.py @@ -23,43 +23,6 @@ import bigframes.session._io.bigquery -@pytest.mark.parametrize( - ("query_or_table", "index_col"), - [ - pytest.param( - "bigquery-public-data.patents_view.ipcr_201708", - (), - id="1g_table_w_default_index", - ), - pytest.param( - "bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2011", - (), - id="30g_table_w_default_index", - ), - # TODO(chelsealin): Disable the long run tests until we have propertily - # ordering support to avoid materializating any data. - # # Adding default index to large tables would take much longer time, - # # e.g. ~5 mins for a 100G table, ~20 mins for a 1T table. - # pytest.param( - # "bigquery-public-data.stackoverflow.post_history", - # ["id"], - # id="100g_table_w_unique_column_index", - # ), - # pytest.param( - # "bigquery-public-data.wise_all_sky_data_release.all_wise", - # ["cntr"], - # id="1t_table_w_unique_column_index", - # ), - ], -) -def test_read_gbq_for_large_tables( - session: bigframes.Session, query_or_table, index_col -): - """Verify read_gbq() is able to read large tables.""" - df = session.read_gbq(query_or_table, index_col=index_col) - assert len(df.columns) != 0 - - def test_close(session: bigframes.Session): # we will create two tables and confirm that they are deleted # when the session is closed From 26ae5e7e769c5bde2d02063f7a475ed5024b6054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Tue, 22 Apr 2025 19:28:36 -0500 Subject: [PATCH 06/28] chore: add tests for `to_gbq` with flexible column names (#1645) * chore: add tests for `to_gbq` with flexible column names * just numbers --- tests/system/small/test_dataframe_io.py | 83 +++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index a69c26bc54..e12db3f598 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -552,6 +552,89 @@ def test_to_gbq_w_duplicate_column_names( ) +def test_to_gbq_w_flexible_column_names( + scalars_df_index, dataset_id: str, bigquery_client +): + """Test the `to_gbq` API when dealing with flexible column names. + + This test is for BigQuery-backed storage nodes. + + See: https://cloud.google.com/bigquery/docs/schemas#flexible-column-names + """ + destination_table = f"{dataset_id}.test_to_gbq_w_flexible_column_names" + renamed_columns = { + # First column in Japanese (tests unicode). + "bool_col": "最初のカラム", + "bytes_col": "col with space", + # Dots aren't allowed in BigQuery column names, so these should be translated + "date_col": "col.with.dots", + "datetime_col": "col-with-hyphens", + "geography_col": "1start_with_number", + "int64_col": "col_with_underscore", + # Just numbers. + "int64_too": "123", + } + bf_df = scalars_df_index[renamed_columns.keys()].rename(columns=renamed_columns) + assert list(bf_df.columns) == list(renamed_columns.values()) + bf_df.to_gbq(destination_table, index=False) + + table = bigquery_client.get_table(destination_table) + columns = [field.name for field in table.schema] + assert columns == [ + "最初のカラム", + "col with space", + # Dots aren't allowed in BigQuery column names, so these should be translated + "col_with_dots", + "col-with-hyphens", + "1start_with_number", + "col_with_underscore", + "123", + ] + + +def test_to_gbq_w_flexible_column_names_local_node( + session, dataset_id: str, bigquery_client +): + """Test the `to_gbq` API when dealing with flexible column names. + + This test is for local nodes, e.g. read_pandas(), since those may go through + a different code path compared to data that starts in BigQuery. + + See: https://cloud.google.com/bigquery/docs/schemas#flexible-column-names + """ + destination_table = f"{dataset_id}.test_to_gbq_w_flexible_column_names_local_node" + + data = { + # First column in Japanese (tests unicode). + "最初のカラム": [1, 2, 3], + "col with space": [4, 5, 6], + # Dots aren't allowed in BigQuery column names, so these should be translated + "col.with.dots": [7, 8, 9], + "col-with-hyphens": [10, 11, 12], + "1start_with_number": [13, 14, 15], + "col_with_underscore": [16, 17, 18], + "123": [19, 20, 21], + } + pd_df = pd.DataFrame(data) + assert list(pd_df.columns) == list(data.keys()) + bf_df = session.read_pandas(pd_df) + assert list(bf_df.columns) == list(data.keys()) + bf_df.to_gbq(destination_table, index=False) + + table = bigquery_client.get_table(destination_table) + columns = [field.name for field in table.schema] + assert columns == [ + "最初のカラム", + "col with space", + # Dots aren't allowed in BigQuery column names, so these should be translated + "col_with_dots", + "col-with-hyphens", + "1start_with_number", + "col_with_underscore", + "123", + ] + + def test_to_gbq_w_None_column_names( scalars_df_index, scalars_pandas_df_index, dataset_id ): From ae83e61c49ade64d6f727e9f364bd2f1aeec6e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Wed, 23 Apr 2025 17:37:56 -0500 Subject: [PATCH 07/28] deps: support shapely 1.8.5+ again (#1651) --- bigframes/bigquery/_operations/geo.py | 12 ++++++--- bigframes/dtypes.py | 2 +- setup.py | 2 +- testing/constraints-3.9.txt | 2 +- tests/system/small/bigquery/test_geo.py | 19 +++++++++++++ tests/unit/core/test_dtypes.py | 8 +++--- tests/unit/core/test_sql.py | 36 ++++++++++++++----------- 7 files changed, 55 insertions(+), 26 deletions(-) diff --git a/bigframes/bigquery/_operations/geo.py b/bigframes/bigquery/_operations/geo.py index 262dca6d6b..0856baabea 100644 --- a/bigframes/bigquery/_operations/geo.py +++ b/bigframes/bigquery/_operations/geo.py @@ -106,7 +106,9 @@ def st_area( def st_difference( series: Union[bigframes.series.Series, bigframes.geopandas.GeoSeries], other: Union[ - bigframes.series.Series, bigframes.geopandas.GeoSeries, shapely.Geometry + bigframes.series.Series, + bigframes.geopandas.GeoSeries, + shapely.geometry.base.BaseGeometry, ], ) -> bigframes.series.Series: """ @@ -207,7 +209,9 @@ def st_difference( def st_distance( series: Union[bigframes.series.Series, bigframes.geopandas.GeoSeries], other: Union[ - bigframes.series.Series, bigframes.geopandas.GeoSeries, shapely.Geometry + bigframes.series.Series, + bigframes.geopandas.GeoSeries, + shapely.geometry.base.BaseGeometry, ], *, use_spheroid: bool = False, @@ -282,7 +286,9 @@ def st_distance( def st_intersection( series: Union[bigframes.series.Series, bigframes.geopandas.GeoSeries], other: Union[ - bigframes.series.Series, bigframes.geopandas.GeoSeries, shapely.Geometry + bigframes.series.Series, + bigframes.geopandas.GeoSeries, + shapely.geometry.base.BaseGeometry, ], ) -> bigframes.series.Series: """ diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index de6c331043..eff9d02c5c 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -607,7 +607,7 @@ def _infer_dtype_from_python_type(type_: type) -> Dtype: return DATE_DTYPE if issubclass(type_, datetime.time): return TIME_DTYPE - if issubclass(type_, shapely.Geometry): + if issubclass(type_, shapely.geometry.base.BaseGeometry): return GEO_DTYPE else: raise TypeError( diff --git a/setup.py b/setup.py index 1fe7006860..edc77e11b6 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ "pyarrow >=15.0.2", "pydata-google-auth >=1.8.2", "requests >=2.27.1", - "shapely >=2.0.0", + "shapely >=1.8.5", "sqlglot >=23.6.3", "tabulate >=0.9", "ipywidgets >=7.7.1", diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index b0537cd035..dff245d176 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -19,7 +19,7 @@ pyarrow==15.0.2 pydata-google-auth==1.8.2 requests==2.27.1 scikit-learn==1.2.2 -shapely==2.0.0 +shapely==1.8.5 sqlglot==23.6.3 tabulate==0.9 ipywidgets==7.7.1 diff --git a/tests/system/small/bigquery/test_geo.py b/tests/system/small/bigquery/test_geo.py index fa2c522109..be517fb5cc 100644 --- a/tests/system/small/bigquery/test_geo.py +++ b/tests/system/small/bigquery/test_geo.py @@ -15,6 +15,7 @@ import geopandas # type: ignore import pandas as pd import pandas.testing +import pytest from shapely.geometry import ( # type: ignore GeometryCollection, LineString, @@ -94,6 +95,12 @@ def test_geo_st_difference_with_geometry_objects(): def test_geo_st_difference_with_single_geometry_object(): + pytest.importorskip( + "shapely", + minversion="2.0.0", + reason="shapely objects must be hashable to include in our expression trees", + ) + data1 = [ Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]), Polygon([(0, 1), (10, 1), (10, 9), (0, 9), (0, 1)]), @@ -205,6 +212,12 @@ def test_geo_st_distance_with_geometry_objects(): def test_geo_st_distance_with_single_geometry_object(): + pytest.importorskip( + "shapely", + minversion="2.0.0", + reason="shapely objects must be hashable to include in our expression trees", + ) + data1 = [ # 0.00001 is approximately 1 meter. Polygon([(0, 0), (0.00001, 0), (0.00001, 0.00001), (0, 0.00001), (0, 0)]), @@ -279,6 +292,12 @@ def test_geo_st_intersection_with_geometry_objects(): def test_geo_st_intersection_with_single_geometry_object(): + pytest.importorskip( + "shapely", + minversion="2.0.0", + reason="shapely objects must be hashable to include in our expression trees", + ) + data1 = [ Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]), Polygon([(0, 1), (10, 1), (10, 9), (0, 9), (0, 1)]), diff --git a/tests/unit/core/test_dtypes.py b/tests/unit/core/test_dtypes.py index bbeac3602b..37658bc436 100644 --- a/tests/unit/core/test_dtypes.py +++ b/tests/unit/core/test_dtypes.py @@ -20,7 +20,7 @@ import pandas as pd import pyarrow as pa # type: ignore import pytest -import shapely # type: ignore +import shapely.geometry # type: ignore import bigframes.core.compile.ibis_types import bigframes.dtypes @@ -231,9 +231,9 @@ def test_bigframes_string_dtype_converts(ibis_dtype, bigframes_dtype_str): (bool, bigframes.dtypes.BOOL_DTYPE), (int, bigframes.dtypes.INT_DTYPE), (str, bigframes.dtypes.STRING_DTYPE), - (shapely.Point, bigframes.dtypes.GEO_DTYPE), - (shapely.Polygon, bigframes.dtypes.GEO_DTYPE), - (shapely.Geometry, bigframes.dtypes.GEO_DTYPE), + (shapely.geometry.Point, bigframes.dtypes.GEO_DTYPE), + (shapely.geometry.Polygon, bigframes.dtypes.GEO_DTYPE), + (shapely.geometry.base.BaseGeometry, bigframes.dtypes.GEO_DTYPE), ], ) def test_bigframes_type_supports_python_types(python_type, expected_dtype): diff --git a/tests/unit/core/test_sql.py b/tests/unit/core/test_sql.py index 1b5c63e061..17da3008fc 100644 --- a/tests/unit/core/test_sql.py +++ b/tests/unit/core/test_sql.py @@ -74,43 +74,45 @@ def test_simple_literal(value, expected_pattern): @pytest.mark.parametrize( - ("value", "expected"), + ("value", "expected_pattern"), ( # Try to have some list of literals for each scalar data type: # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types - ([None, None], "[NULL, NULL]"), - ([True, False], "[True, False]"), + ([None, None], re.escape("[NULL, NULL]")), + ([True, False], re.escape("[True, False]")), ( [b"\x01\x02\x03ABC", b"\x01\x02\x03ABC"], - "[b'\\x01\\x02\\x03ABC', b'\\x01\\x02\\x03ABC']", + re.escape("[b'\\x01\\x02\\x03ABC', b'\\x01\\x02\\x03ABC']"), ), ( [datetime.date(2025, 1, 1), datetime.date(2025, 1, 1)], - "[DATE('2025-01-01'), DATE('2025-01-01')]", + re.escape("[DATE('2025-01-01'), DATE('2025-01-01')]"), ), ( [datetime.datetime(2025, 1, 2, 3, 45, 6, 789123)], - "[DATETIME('2025-01-02T03:45:06.789123')]", + re.escape("[DATETIME('2025-01-02T03:45:06.789123')]"), ), ( - [shapely.Point(0, 1), shapely.Point(0, 2)], - "[ST_GEOGFROMTEXT('POINT (0 1)'), ST_GEOGFROMTEXT('POINT (0 2)')]", + [shapely.geometry.Point(0, 1), shapely.geometry.Point(0, 2)], + r"\[ST_GEOGFROMTEXT\('POINT \(0[.]?0* 1[.]?0*\)'\), ST_GEOGFROMTEXT\('POINT \(0[.]?0* 2[.]?0*\)'\)\]", ), # TODO: INTERVAL type (e.g. from dateutil.relativedelta) # TODO: JSON type (TBD what Python object that would correspond to) - ([123, 456], "[123, 456]"), + ([123, 456], re.escape("[123, 456]")), ( [decimal.Decimal("123.75"), decimal.Decimal("456.78")], - "[CAST('123.75' AS NUMERIC), CAST('456.78' AS NUMERIC)]", + re.escape("[CAST('123.75' AS NUMERIC), CAST('456.78' AS NUMERIC)]"), ), # TODO: support BIGNUMERIC by looking at precision/scale of the DECIMAL - ([123.75, 456.78], "[123.75, 456.78]"), + ([123.75, 456.78], re.escape("[123.75, 456.78]")), # TODO: support RANGE type - (["abc", "def"], "['abc', 'def']"), + (["abc", "def"], re.escape("['abc', 'def']")), # TODO: support STRUCT type (possibly another method?) ( [datetime.time(12, 34, 56, 789123), datetime.time(11, 25, 56, 789123)], - "[TIME(DATETIME('1970-01-01 12:34:56.789123')), TIME(DATETIME('1970-01-01 11:25:56.789123'))]", + re.escape( + "[TIME(DATETIME('1970-01-01 12:34:56.789123')), TIME(DATETIME('1970-01-01 11:25:56.789123'))]" + ), ), ( [ @@ -121,13 +123,15 @@ def test_simple_literal(value, expected_pattern): 2025, 2, 1, 4, 45, 6, 789123, tzinfo=datetime.timezone.utc ), ], - "[TIMESTAMP('2025-01-02T03:45:06.789123+00:00'), TIMESTAMP('2025-02-01T04:45:06.789123+00:00')]", + re.escape( + "[TIMESTAMP('2025-01-02T03:45:06.789123+00:00'), TIMESTAMP('2025-02-01T04:45:06.789123+00:00')]" + ), ), ), ) -def test_simple_literal_w_list(value: list, expected: str): +def test_simple_literal_w_list(value: list, expected_pattern: str): got = sql.simple_literal(value) - assert got == expected + assert re.match(expected_pattern, got) is not None def test_create_vector_search_sql_simple(): From e84f232b0fc5e2167a7cddb355cf0c8837ae5422 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Wed, 23 Apr 2025 16:52:15 -0700 Subject: [PATCH 08/28] feat: Short circuit query for local scan (#1618) --- bigframes/core/local_data.py | 103 ++++++++++++++--------- bigframes/core/rewrite/__init__.py | 6 +- bigframes/core/rewrite/scan_reduction.py | 19 ++++- bigframes/dtypes.py | 15 +++- bigframes/session/bq_caching_executor.py | 3 +- bigframes/session/local_scan_executor.py | 68 +++++++++++++++ tests/system/small/bigquery/test_json.py | 2 +- tests/system/small/test_session.py | 9 +- tests/unit/test_local_engine.py | 2 +- 9 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 bigframes/session/local_scan_executor.py diff --git a/bigframes/core/local_data.py b/bigframes/core/local_data.py index e5c67fcf43..1330d04589 100644 --- a/bigframes/core/local_data.py +++ b/bigframes/core/local_data.py @@ -31,7 +31,6 @@ import pyarrow.parquet # type: ignore import bigframes.core.schema as schemata -import bigframes.core.utils as utils import bigframes.dtypes @@ -79,7 +78,7 @@ def from_pandas(cls, dataframe: pd.DataFrame) -> ManagedArrowTable: mat = ManagedArrowTable( pa.table(columns, names=column_names), schemata.ArraySchema(tuple(fields)) ) - mat.validate(include_content=True) + mat.validate() return mat @classmethod @@ -98,15 +97,14 @@ def from_pyarrow(self, table: pa.Table) -> ManagedArrowTable: mat.validate() return mat - def to_parquet( + def to_pyarrow_table( self, - dst: Union[str, io.IOBase], *, offsets_col: Optional[str] = None, geo_format: Literal["wkb", "wkt"] = "wkt", duration_type: Literal["int", "duration"] = "duration", json_type: Literal["string"] = "string", - ): + ) -> pa.Table: pa_table = self.data if offsets_col is not None: pa_table = pa_table.append_column( @@ -119,6 +117,23 @@ def to_parquet( f"duration as {duration_type} not yet implemented" ) assert json_type == "string" + return pa_table + + def to_parquet( + self, + dst: Union[str, io.IOBase], + *, + offsets_col: Optional[str] = None, + geo_format: Literal["wkb", "wkt"] = "wkt", + duration_type: Literal["int", "duration"] = "duration", + json_type: Literal["string"] = "string", + ): + pa_table = self.to_pyarrow_table( + offsets_col=offsets_col, + geo_format=geo_format, + duration_type=duration_type, + json_type=json_type, + ) pyarrow.parquet.write_table(pa_table, where=dst) def itertuples( @@ -142,7 +157,7 @@ def itertuples( ): yield tuple(row_dict.values()) - def validate(self, include_content: bool = False): + def validate(self): for bf_field, arrow_field in zip(self.schema.items, self.data.schema): expected_arrow_type = _get_managed_storage_type(bf_field.dtype) arrow_type = arrow_field.type @@ -151,38 +166,6 @@ def validate(self, include_content: bool = False): f"Field {bf_field} has arrow array type: {arrow_type}, expected type: {expected_arrow_type}" ) - if include_content: - for batch in self.data.to_batches(): - for field in self.schema.items: - _validate_content(batch.column(field.column), field.dtype) - - -def _validate_content(array: pa.Array, dtype: bigframes.dtypes.Dtype): - """ - Recursively validates the content of a PyArrow Array based on the - expected BigFrames dtype, focusing on complex types like JSON, structs, - and arrays where the Arrow type alone isn't sufficient. - """ - # TODO: validate GEO data context. - if dtype == bigframes.dtypes.JSON_DTYPE: - values = array.to_pandas() - for data in values: - # Skip scalar null values to avoid `TypeError` from json.load. - if not utils.is_list_like(data) and pd.isna(data): - continue - try: - # Attempts JSON parsing. - json.loads(data) - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON format found: {data!r}") from e - elif bigframes.dtypes.is_struct_like(dtype): - for field_name, dtype in bigframes.dtypes.get_struct_fields(dtype).items(): - _validate_content(array.field(field_name), dtype) - elif bigframes.dtypes.is_array_like(dtype): - return _validate_content( - array.flatten(), bigframes.dtypes.get_array_inner_type(dtype) - ) - # Sequential iterator, but could split into batches and leverage parallelism for speed def _iter_table( @@ -280,6 +263,34 @@ def _adapt_pandas_series( def _adapt_arrow_array( array: Union[pa.ChunkedArray, pa.Array] ) -> tuple[Union[pa.ChunkedArray, pa.Array], bigframes.dtypes.Dtype]: + """Normalize the array to managed storage types. Preverse shapes, only transforms values.""" + if pa.types.is_struct(array.type): + assert isinstance(array, pa.StructArray) + assert isinstance(array.type, pa.StructType) + arrays = [] + dtypes = [] + pa_fields = [] + for i in range(array.type.num_fields): + field_array, field_type = _adapt_arrow_array(array.field(i)) + arrays.append(field_array) + dtypes.append(field_type) + pa_fields.append(pa.field(array.type.field(i).name, field_array.type)) + struct_array = pa.StructArray.from_arrays( + arrays=arrays, fields=pa_fields, mask=array.is_null() + ) + dtype = bigframes.dtypes.struct_type( + [(field.name, dtype) for field, dtype in zip(pa_fields, dtypes)] + ) + return struct_array, dtype + if pa.types.is_list(array.type): + assert isinstance(array, pa.ListArray) + values, values_type = _adapt_arrow_array(array.values) + new_value = pa.ListArray.from_arrays( + array.offsets, values, mask=array.is_null() + ) + return new_value.fill_null([]), bigframes.dtypes.list_type(values_type) + if array.type == bigframes.dtypes.JSON_ARROW_TYPE: + return _canonicalize_json(array), bigframes.dtypes.JSON_DTYPE target_type = _logical_type_replacements(array.type) if target_type != array.type: # TODO: Maybe warn if lossy conversion? @@ -292,6 +303,22 @@ def _adapt_arrow_array( return array, bf_type +def _canonicalize_json(array: pa.Array) -> pa.Array: + def _canonicalize_scalar(json_string): + if json_string is None: + return None + # This is the canonical form that bq uses when emitting json + # The sorted keys and unambiguous whitespace ensures a 1:1 mapping + # between syntax and semantics. + return json.dumps( + json.loads(json_string), sort_keys=True, separators=(",", ":") + ) + + return pa.array( + [_canonicalize_scalar(value) for value in array.to_pylist()], type=pa.string() + ) + + def _get_managed_storage_type(dtype: bigframes.dtypes.Dtype) -> pa.DataType: if dtype in _MANAGED_STORAGE_TYPES_OVERRIDES.keys(): return _MANAGED_STORAGE_TYPES_OVERRIDES[dtype] diff --git a/bigframes/core/rewrite/__init__.py b/bigframes/core/rewrite/__init__.py index 555199dcd9..e9a94069f5 100644 --- a/bigframes/core/rewrite/__init__.py +++ b/bigframes/core/rewrite/__init__.py @@ -17,7 +17,10 @@ from bigframes.core.rewrite.legacy_align import legacy_join_as_projection from bigframes.core.rewrite.order import bake_order, defer_order from bigframes.core.rewrite.pruning import column_pruning -from bigframes.core.rewrite.scan_reduction import try_reduce_to_table_scan +from bigframes.core.rewrite.scan_reduction import ( + try_reduce_to_local_scan, + try_reduce_to_table_scan, +) from bigframes.core.rewrite.slices import pull_up_limits, rewrite_slice from bigframes.core.rewrite.timedeltas import rewrite_timedelta_expressions from bigframes.core.rewrite.windows import rewrite_range_rolling @@ -34,4 +37,5 @@ "rewrite_range_rolling", "try_reduce_to_table_scan", "bake_order", + "try_reduce_to_local_scan", ] diff --git a/bigframes/core/rewrite/scan_reduction.py b/bigframes/core/rewrite/scan_reduction.py index be8db4827c..b9050c0c34 100644 --- a/bigframes/core/rewrite/scan_reduction.py +++ b/bigframes/core/rewrite/scan_reduction.py @@ -28,6 +28,20 @@ def try_reduce_to_table_scan(root: nodes.BigFrameNode) -> Optional[nodes.ReadTab return None +def try_reduce_to_local_scan(node: nodes.BigFrameNode) -> Optional[nodes.ReadLocalNode]: + if not all( + map( + lambda x: isinstance(x, (nodes.ReadLocalNode, nodes.SelectionNode)), + node.unique_nodes(), + ) + ): + return None + result = node.bottom_up(merge_scan) + if isinstance(result, nodes.ReadLocalNode): + return result + return None + + @functools.singledispatch def merge_scan(node: nodes.BigFrameNode) -> nodes.BigFrameNode: return node @@ -35,11 +49,12 @@ def merge_scan(node: nodes.BigFrameNode) -> nodes.BigFrameNode: @merge_scan.register def _(node: nodes.SelectionNode) -> nodes.BigFrameNode: - if not isinstance(node.child, nodes.ReadTableNode): + if not isinstance(node.child, (nodes.ReadTableNode, nodes.ReadLocalNode)): return node if node.has_multi_referenced_ids: return node - + if isinstance(node, nodes.ReadLocalNode) and node.offsets_col is not None: + return node selection = { aliased_ref.ref.id: aliased_ref.id for aliased_ref in node.input_output_pairs } diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index eff9d02c5c..ec115a93d0 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -19,7 +19,7 @@ import decimal import textwrap import typing -from typing import Any, Dict, List, Literal, Union +from typing import Any, Dict, List, Literal, Sequence, Union import bigframes_vendored.constants as constants import db_dtypes # type: ignore @@ -370,6 +370,19 @@ def get_array_inner_type(type_: ExpressionType) -> Dtype: return arrow_dtype_to_bigframes_dtype(list_type.value_type) +def list_type(values_type: Dtype) -> Dtype: + """Create a list dtype with given value type.""" + return pd.ArrowDtype(pa.list_(bigframes_dtype_to_arrow_dtype(values_type))) + + +def struct_type(fields: Sequence[tuple[str, Dtype]]) -> Dtype: + """Create a struct dtype with give fields names and types.""" + pa_fields = [ + pa.field(str, bigframes_dtype_to_arrow_dtype(dtype)) for str, dtype in fields + ] + return pd.ArrowDtype(pa.struct(pa_fields)) + + _ORDERABLE_SIMPLE_TYPES = set( mapping.dtype for mapping in SIMPLE_TYPES if mapping.orderable ) diff --git a/bigframes/session/bq_caching_executor.py b/bigframes/session/bq_caching_executor.py index 983b1918f5..584b41452a 100644 --- a/bigframes/session/bq_caching_executor.py +++ b/bigframes/session/bq_caching_executor.py @@ -35,7 +35,7 @@ import bigframes.dtypes import bigframes.exceptions as bfe import bigframes.features -from bigframes.session import executor, read_api_execution +from bigframes.session import executor, local_scan_executor, read_api_execution import bigframes.session._io.bigquery as bq_io import bigframes.session.metrics import bigframes.session.planner @@ -84,6 +84,7 @@ def __init__( bqstoragereadclient=bqstoragereadclient, project=self.bqclient.project, ), + local_scan_executor.LocalScanExecutor(), ) def to_sql( diff --git a/bigframes/session/local_scan_executor.py b/bigframes/session/local_scan_executor.py new file mode 100644 index 0000000000..3ed1b759c5 --- /dev/null +++ b/bigframes/session/local_scan_executor.py @@ -0,0 +1,68 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from typing import Optional + +from bigframes.core import bigframe_node, rewrite +from bigframes.session import executor, semi_executor + + +class LocalScanExecutor(semi_executor.SemiExecutor): + """ + Executes plans reducible to a arrow table scan. + """ + + def execute( + self, + plan: bigframe_node.BigFrameNode, + ordered: bool, + peek: Optional[int] = None, + ) -> Optional[executor.ExecuteResult]: + node = rewrite.try_reduce_to_local_scan(plan) + if not node: + return None + + # TODO: Can support some slicing, sorting + def iterator_supplier(): + offsets_col = ( + node.offsets_col.sql if (node.offsets_col is not None) else None + ) + arrow_table = node.local_data_source.to_pyarrow_table( + offsets_col=offsets_col + ) + if peek: + arrow_table = arrow_table.slice(0, peek) + + needed_cols = [item.source_id for item in node.scan_list.items] + if offsets_col is not None: + needed_cols.append(offsets_col) + + arrow_table = arrow_table.select(needed_cols) + arrow_table = arrow_table.rename_columns( + {item.source_id: item.id.sql for item in node.scan_list.items} + ) + yield from arrow_table.to_batches() + + total_rows = node.row_count + if (peek is not None) and (total_rows is not None): + total_rows = min(peek, total_rows) + + return executor.ExecuteResult( + arrow_batches=iterator_supplier, + schema=plan.schema, + query_job=None, + total_bytes=None, + total_rows=total_rows, + ) diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index 00f690ed54..df5a524b55 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -66,7 +66,7 @@ def test_json_set_w_more_pairs(): s, json_path_value_pairs=[("$.a", 1), ("$.b", 2), ("$.a", [3, 4, 5])] ) - expected_json = ['{"a": 3, "b": 2}', '{"a": 4, "b": 2}', '{"a": 5, "b": 2, "c": 1}'] + expected_json = ['{"a": 3,"b":2}', '{"a":4,"b": 2}', '{"a": 5,"b":2,"c":1}'] expected = bpd.Series(expected_json, dtype=dtypes.JSON_DTYPE) pd.testing.assert_series_equal(actual.to_pandas(), expected.to_pandas()) diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index c7e7fa3573..668c764645 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import io +import json import random import re import tempfile @@ -32,7 +33,6 @@ import pytest import bigframes -import bigframes.core.indexes.base import bigframes.dataframe import bigframes.dtypes import bigframes.ml.linear_model @@ -990,10 +990,7 @@ def test_read_pandas_json_series_w_invalid_json(session, write_engine): ] pd_s = pd.Series(json_data, dtype=bigframes.dtypes.JSON_DTYPE) - with pytest.raises( - ValueError, - match="Invalid JSON format found", - ): + with pytest.raises(json.JSONDecodeError): session.read_pandas(pd_s, write_engine=write_engine) @@ -1101,7 +1098,7 @@ def test_read_pandas_w_nested_invalid_json(session, write_engine): ), ) - with pytest.raises(ValueError, match="Invalid JSON format found"): + with pytest.raises(json.JSONDecodeError): session.read_pandas(pd_s, write_engine=write_engine) diff --git a/tests/unit/test_local_engine.py b/tests/unit/test_local_engine.py index d4e0dae1f3..b4672d07a9 100644 --- a/tests/unit/test_local_engine.py +++ b/tests/unit/test_local_engine.py @@ -41,7 +41,7 @@ def small_inline_frame() -> pd.DataFrame: "bools": pd.Series([True, None, False], dtype="boolean"), "strings": pd.Series(["b", "aa", "ccc"], dtype="string[pyarrow]"), "intLists": pd.Series( - [[1, 2, 3], [4, 5, 6, 7], None], + [[1, 2, 3], [4, 5, 6, 7], []], dtype=pd.ArrowDtype(pa.list_(pa.int64())), ), }, From 9662745265c8c6e42f372629bd2c7806542cee1a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 24 Apr 2025 12:14:44 -0700 Subject: [PATCH 09/28] fix: Remove itertools.pairwise usage (#1638) --- bigframes/core/local_data.py | 15 +++++++++++- tests/unit/test_local_data.py | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_local_data.py diff --git a/bigframes/core/local_data.py b/bigframes/core/local_data.py index 1330d04589..baa29ba72b 100644 --- a/bigframes/core/local_data.py +++ b/bigframes/core/local_data.py @@ -212,7 +212,7 @@ def _( value_generator = iter_array( array.flatten(), bigframes.dtypes.get_array_inner_type(dtype) ) - for (start, end) in itertools.pairwise(array.offsets): + for (start, end) in _pairwise(array.offsets): arr_size = end.as_py() - start.as_py() yield list(itertools.islice(value_generator, arr_size)) @@ -389,3 +389,16 @@ def _physical_type_replacements(dtype: pa.DataType) -> pa.DataType: if dtype in _ARROW_MANAGED_STORAGE_OVERRIDES: return _ARROW_MANAGED_STORAGE_OVERRIDES[dtype] return dtype + + +def _pairwise(iterable): + do_yield = False + a = None + b = None + for item in iterable: + a = b + b = item + if do_yield: + yield (a, b) + else: + do_yield = True diff --git a/tests/unit/test_local_data.py b/tests/unit/test_local_data.py new file mode 100644 index 0000000000..9cd08787c9 --- /dev/null +++ b/tests/unit/test_local_data.py @@ -0,0 +1,46 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pandas as pd +import pandas.testing +import pyarrow as pa + +from bigframes import dtypes +from bigframes.core import local_data + +pd_data = pd.DataFrame( + { + "ints": [10, 20, 30, 40], + "nested_ints": [[1, 2], [3, 4, 5], [], [20, 30]], + "structs": [{"a": 100}, {}, {"b": 200}, {"b": 300}], + } +) + +pd_data_normalized = pd.DataFrame( + { + "ints": pd.Series([10, 20, 30, 40], dtype=dtypes.INT_DTYPE), + "nested_ints": pd.Series( + [[1, 2], [3, 4, 5], [], [20, 30]], dtype=pd.ArrowDtype(pa.list_(pa.int64())) + ), + "structs": pd.Series( + [{"a": 100}, {}, {"b": 200}, {"b": 300}], + dtype=pd.ArrowDtype(pa.struct({"a": pa.int64(), "b": pa.int64()})), + ), + } +) + + +def test_local_data_well_formed_round_trip(): + local_entry = local_data.ManagedArrowTable.from_pandas(pd_data) + result = pd.DataFrame(local_entry.itertuples(), columns=pd_data.columns) + pandas.testing.assert_frame_equal(pd_data_normalized, result, check_dtype=False) From 9128c4a31dab487bc23f67c43380abd0beda5b1c Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Thu, 24 Apr 2025 13:32:51 -0700 Subject: [PATCH 10/28] docs: add JSON data types notebook (#1647) --- .../array.ipynb} | 461 +++++------------ notebooks/data_types/json.ipynb | 451 ++++++++++++++++ notebooks/data_types/struct.ipynb | 483 ++++++++++++++++++ 3 files changed, 1064 insertions(+), 331 deletions(-) rename notebooks/{dataframes/struct_and_array_dtypes.ipynb => data_types/array.ipynb} (50%) create mode 100644 notebooks/data_types/json.ipynb create mode 100644 notebooks/data_types/struct.ipynb diff --git a/notebooks/dataframes/struct_and_array_dtypes.ipynb b/notebooks/data_types/array.ipynb similarity index 50% rename from notebooks/dataframes/struct_and_array_dtypes.ipynb rename to notebooks/data_types/array.ipynb index def65ee6ca..96c5da5ac6 100644 --- a/notebooks/dataframes/struct_and_array_dtypes.ipynb +++ b/notebooks/data_types/array.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Copyright 2024 Google LLC\n", + "# Copyright 2025 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", @@ -25,16 +25,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# A Guide to Array and Struct Data Types in BigQuery DataFrames" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Set up your environment\n", + "# Array Data Types\n", + "\n", + "In BigQuery, an [ARRAY](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#array_type) (also called a `repeated` column) is an ordered list of zero or more elements of the same, non-`NULL` data type. It's important to note that BigQuery `ARRAY`s cannot contain nested `ARRAY`s. BigQuery DataFrames represents BigQuery `ARRAY` types to `pandas.ArrowDtype(pa.list_(T))`, where `T` is the underlying Arrow type of the array elements.\n", "\n", - "To get started, follow the instructions in the notebooks within the `getting_started` folder to set up your environment. Once your environment is ready, you can import the necessary packages by running the following code:" + "This notebook illustrates how to work with `ARRAY` columns in BigQuery DataFrames. First, let's import the required packages and perform the necessary setup below." ] }, { @@ -45,6 +40,7 @@ "source": [ "import bigframes.pandas as bpd\n", "import bigframes.bigquery as bbq\n", + "import pandas as pd\n", "import pyarrow as pa" ] }, @@ -64,20 +60,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Array Data Types\n", - "\n", - "In BigQuery, an [array](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#array_type) (also called a repeated column) is an ordered list of zero or more elements of the same data type. Arrays cannot contain other arrays or `NULL` elements.\n", + "## Create DataFrames with an array column\n", "\n", - "BigQuery DataFrames map BigQuery array types to `pandas.ArrowDtype(pa.list_())`. The following code examples illustrate how to work with array columns in BigQuery DataFrames." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create DataFrames with array columns\n", - "\n", - "Create a DataFrame in BigQuery DataFrames from local sample data. Use a list of lists to create a column with the `list[pyarrow]` dtype, which corresponds to the `ARRAY` type in BigQuery." + "**Example 1: Creating from a list of lists/tuples**" ] }, { @@ -146,10 +131,13 @@ } ], "source": [ - "df = bpd.DataFrame({\n", - " 'Name': ['Alice', 'Bob', 'Charlie'],\n", - " 'Scores': [[95, 88, 92], [78, 81], [82, 89, 94, 100]],\n", - "})\n", + "names = [\"Alice\", \"Bob\", \"Charlie\"]\n", + "scores = [\n", + " [95, 88, 92],\n", + " [78, 81],\n", + " [82, 89, 94, 100]\n", + "]\n", + "df = bpd.DataFrame({\"Name\": names, \"Scores\": scores})\n", "df" ] }, @@ -179,9 +167,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Operate on array data\n", - "\n", - "While pandas offers vectorized operations and lambda expressions for array manipulation, BigQuery DataFrames leverages the computational power of BigQuery itself. You can access a variety of native BigQuery array operations, such as [`array_agg`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_agg) and [`array_length`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_length), through the [`bigframes.bigquery`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery) package (abbreviated as `bbq` in the following code samples)." + "**Example 2: Defining schema explicitly**" ] }, { @@ -192,10 +178,10 @@ { "data": { "text/plain": [ - "0 3\n", - "1 2\n", - "2 4\n", - "Name: Scores, dtype: Int64" + "0 [95. 88. 92.]\n", + "1 [78. 81.]\n", + "2 [ 82. 89. 94. 100.]\n", + "dtype: list[pyarrow]" ] }, "execution_count": 6, @@ -204,8 +190,14 @@ } ], "source": [ - "# Find the length in each array.\n", - "bbq.array_length(df['Scores'])" + "bpd.Series(data=scores, dtype=pd.ArrowDtype(pa.list_(pa.float64())))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 3: Reading from a source**" ] }, { @@ -216,10 +208,12 @@ { "data": { "text/plain": [ - "0 3\n", - "1 2\n", - "2 4\n", - "Name: Scores, dtype: Int64" + "0 [{'tables': {'score': 0.9349926710128784, 'val...\n", + "1 [{'tables': {'score': 0.9690881371498108, 'val...\n", + "2 [{'tables': {'score': 0.8667634129524231, 'val...\n", + "3 [{'tables': {'score': 0.9351968765258789, 'val...\n", + "4 [{'tables': {'score': 0.8572560548782349, 'val...\n", + "Name: predicted_default_payment_next_month, dtype: list>>[pyarrow]" ] }, "execution_count": 7, @@ -228,389 +222,188 @@ } ], "source": [ - "# Find the length of each array with list accessor\n", - "df['Scores'].list.len()" + "bpd.read_gbq(\"bigquery-public-data.ml_datasets.credit_card_default\", max_results=5)[\"predicted_default_payment_next_month\"]" ] }, { - "cell_type": "code", - "execution_count": 8, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 88\n", - "1 81\n", - "2 89\n", - "Name: Scores, dtype: Int64" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "# Find the second element in each array with list accessor\n", - "df['Scores'].list[1]" + "## Operate on `ARRAY` data\n", + "\n", + "BigQuery DataFrames provides two main approaches for operating on list (`ARRAY`) data:\n", + "\n", + "1. **The `Series.list` accessor**: Provides Pandas-like methods for array column manipulation.\n", + "2. **[BigQuery built-in functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions)**: Allows you to use functions mirroring BigQuery SQL operations, available through the `bigframes.bigquery` module (abbreviated as `bbq` below), such as [`array_agg`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_agg) and [`array_length`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_array_length)." ] }, { - "cell_type": "code", - "execution_count": 9, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 95\n", - "0 88\n", - "0 92\n", - "1 78\n", - "1 81\n", - "2 82\n", - "2 89\n", - "2 94\n", - "2 100\n", - "Name: Scores, dtype: Int64" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "# Transforms array elements into individual rows, preserving original order when in ordering\n", - "# mode. If an array has multiple elements, exploded rows are ordered by the element's index\n", - "# within its original array.\n", - "scores = df['Scores'].explode()\n", - "scores" + "### Get the Length of Each Arrray\n", + "\n", + "**Example 1: Using list accessor to get array length**" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 100.0\n", - "0 93.0\n", - "0 97.0\n", - "1 83.0\n", - "1 86.0\n", - "2 87.0\n", - "2 94.0\n", - "2 99.0\n", - "2 105.0\n", - "Name: Scores, dtype: Float64" + "0 3\n", + "1 2\n", + "2 4\n", + "Name: Scores, dtype: Int64" ] }, - "execution_count": 10, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Adjust the scores.\n", - "adj_scores = scores + 5.0\n", - "adj_scores" + "df['Scores'].list.len()" ] }, { - "cell_type": "code", - "execution_count": 11, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 [100. 93. 97.]\n", - "1 [83. 86.]\n", - "2 [ 87. 94. 99. 105.]\n", - "Name: Scores, dtype: list[pyarrow]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "# Aggregate adjusted scores back into arrays.\n", - "adj_scores_arr = bbq.array_agg(adj_scores.groupby(level=0))\n", - "adj_scores_arr" + "**Example 2: Using BigQuery build-in functions to get array length**" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
NameScoresNewScores
0Alice[95 88 92][100. 93. 97.]
1Bob[78 81][83. 86.]
2Charlie[ 82 89 94 100][ 87. 94. 99. 105.]
\n", - "

3 rows × 3 columns

\n", - "
[3 rows x 3 columns in total]" - ], "text/plain": [ - " Name Scores NewScores\n", - "0 Alice [95 88 92] [100. 93. 97.]\n", - "1 Bob [78 81] [83. 86.]\n", - "2 Charlie [ 82 89 94 100] [ 87. 94. 99. 105.]\n", - "\n", - "[3 rows x 3 columns]" + "0 3\n", + "1 2\n", + "2 4\n", + "Name: Scores, dtype: Int64" ] }, - "execution_count": 12, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Add adjusted scores into the DataFrame. This operation requires an implicit join \n", - "# between the two tables, necessitating a unique index in the DataFrame (guaranteed \n", - "# in the default ordering and index mode).\n", - "df['NewScores'] = adj_scores_arr\n", - "df" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Struct Data Types\n", - "\n", - "In BigQuery, a [struct](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type) (also known as a `record`) is a collection of ordered fields, each with a defined data type (required) and an optional field name. BigQuery DataFrames maps BigQuery struct types to the pandas equivalent, `pandas.ArrowDtype(pa.struct())`. This section provides practical code examples illustrating how to use struct columns with BigQuery DataFrames." + "bbq.array_length(df['Scores'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Create DataFrames with struct columns \n", - "\n", - "Create a DataFrame with an `Address` struct column by using dictionaries for the data and setting the dtype to `struct[pyarrow]`." + "### Access Element at a Specific Index (e.g., First Element) " ] }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/sycai/src/python-bigquery-dataframes/venv/lib/python3.11/site-packages/google/cloud/bigquery/_pandas_helpers.py:570: UserWarning: Pyarrow could not determine the type of columns: bigframes_unnamed_index.\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
NameAddress
0Alice{'City': 'New York', 'State': 'NY'}
1Bob{'City': 'San Francisco', 'State': 'CA'}
2Charlie{'City': 'Seattle', 'State': 'WA'}
\n", - "

3 rows × 2 columns

\n", - "
[3 rows x 2 columns in total]" - ], - "text/plain": [ - " Name Address\n", - "0 Alice {'City': 'New York', 'State': 'NY'}\n", - "1 Bob {'City': 'San Francisco', 'State': 'CA'}\n", - "2 Charlie {'City': 'Seattle', 'State': 'WA'}\n", - "\n", - "[3 rows x 2 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "names = bpd.Series(['Alice', 'Bob', 'Charlie'])\n", - "address = bpd.Series(\n", - " [\n", - " {'City': 'New York', 'State': 'NY'},\n", - " {'City': 'San Francisco', 'State': 'CA'},\n", - " {'City': 'Seattle', 'State': 'WA'}\n", - " ],\n", - " dtype=bpd.ArrowDtype(pa.struct(\n", - " [('City', pa.string()), ('State', pa.string())]\n", - " )))\n", - "\n", - "df = bpd.DataFrame({'Name': names, 'Address': address})\n", - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Name string[pyarrow]\n", - "Address struct[pyarrow]\n", - "dtype: object" + "0 95\n", + "1 78\n", + "2 82\n", + "Name: Scores, dtype: Int64" ] }, - "execution_count": 14, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "df.dtypes" + "df['Scores'].list[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Operate on struct data\n", + "### Explode/Unnest Array elements into Seperate Rows\n", "\n", - "Similar to pandas, BigQuery DataFrames provides a [`StructAccessor`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.structs.StructAccessor). Use the methods provided in this accessor to manipulate struct data." + "The exploded rows preserving original order when in ordering mode. If an array has multiple elements, exploded rows are ordered by the element's index\n", + "within its original array. " ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "City string[pyarrow]\n", - "State string[pyarrow]\n", - "dtype: object" + "0 95\n", + "0 88\n", + "0 92\n", + "1 78\n", + "1 81\n", + "2 82\n", + "2 89\n", + "2 94\n", + "2 100\n", + "Name: Scores, dtype: Int64" ] }, - "execution_count": 15, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Return the dtype object of each child field of the struct.\n", - "df['Address'].struct.dtypes()" + "scores = df['Scores'].explode()\n", + "scores" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Aggregate elements back into an array" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 New York\n", - "1 San Francisco\n", - "2 Seattle\n", - "Name: City, dtype: string" + "0 [100. 93. 97.]\n", + "1 [83. 86.]\n", + "2 [ 87. 94. 99. 105.]\n", + "Name: Scores, dtype: list[pyarrow]" ] }, - "execution_count": 16, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Extract a child field as a Series\n", - "city = df['Address'].struct.field(\"City\")\n", - "city" + "new_scores = scores + 5.0\n", + "new_scores_arr = bbq.array_agg(new_scores.groupby(level=0))\n", + "new_scores_arr" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -634,49 +427,55 @@ " \n", " \n", " \n", - " City\n", - " State\n", + " Name\n", + " Scores\n", + " NewScores\n", " \n", " \n", " \n", " \n", " 0\n", - " New York\n", - " NY\n", + " Alice\n", + " [95 88 92]\n", + " [100. 93. 97.]\n", " \n", " \n", " 1\n", - " San Francisco\n", - " CA\n", + " Bob\n", + " [78 81]\n", + " [83. 86.]\n", " \n", " \n", " 2\n", - " Seattle\n", - " WA\n", + " Charlie\n", + " [ 82 89 94 100]\n", + " [ 87. 94. 99. 105.]\n", " \n", " \n", "\n", - "

3 rows × 2 columns

\n", - "[3 rows x 2 columns in total]" + "

3 rows × 3 columns

\n", + "[3 rows x 3 columns in total]" ], "text/plain": [ - " City State\n", - "0 New York NY\n", - "1 San Francisco CA\n", - "2 Seattle WA\n", + " Name Scores NewScores\n", + "0 Alice [95 88 92] [100. 93. 97.]\n", + "1 Bob [78 81] [83. 86.]\n", + "2 Charlie [ 82 89 94 100] [ 87. 94. 99. 105.]\n", "\n", - "[3 rows x 2 columns]" + "[3 rows x 3 columns]" ] }, - "execution_count": 17, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Extract all child fields of a struct as a DataFrame.\n", - "address_df = df['Address'].struct.explode()\n", - "address_df" + "# Add adjusted scores into the DataFrame. This operation requires an implicit join \n", + "# between the two tables, necessitating a unique index in the DataFrame (guaranteed \n", + "# in the default ordering and index mode).\n", + "df['NewScores'] = new_scores_arr\n", + "df" ] } ], @@ -696,7 +495,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.1" } }, "nbformat": 4, diff --git a/notebooks/data_types/json.ipynb b/notebooks/data_types/json.ipynb new file mode 100644 index 0000000000..f0a8ed4ffe --- /dev/null +++ b/notebooks/data_types/json.ipynb @@ -0,0 +1,451 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# JSON Data Types\n", + "\n", + "When using BigQuery DataFrames, columns containing data in BigQuery's [JSON](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#json_type) format (a lightweight standard) are represented as `pandas.ArrowDtype`. The exact underlying Arrow type depends on your library versions. Older environments typically use `db_dtypes.JSONArrowType()` for compatibility, which is an Arrow extension type acting as a light wrapper around `pa.string()`. In contrast, newer setups (pandas 3.0+ and pyarrow 19.0+) utilize the more recent `pa.json_(pa.string())` representation." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "import bigframes.bigquery as bbq\n", + "import db_dtypes\n", + "import pandas as pd\n", + "import pyarrow as pa" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}\n", + "\n", + "bpd.options.display.progress_bar = None\n", + "bpd.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Series with JSON columns\n", + "\n", + "**Example 1: Create a Series with a JSON dtype from local data**\n", + "\n", + "This example demonstrates creating a JSON Series from a list of JSON strings. Note that BigQuery standardizes these strings, for instance, by removing extra spaces and ordering dictionary keys. Specifying the `dtype` is essential; if omitted, a string-type Series will be generated." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1\n", + "1 \"str\"\n", + "2 false\n", + "3 [\"a\",{\"b\":1},null]\n", + "4 {\"a\":{\"b\":[1,2,3],\"c\":true}}\n", + "5 \n", + "dtype: extension>[pyarrow]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "json_data = [\n", + " \"1\",\n", + " '\"str\"',\n", + " \"false\",\n", + " '[\"a\", {\"b\": 1}, null]',\n", + " '{\"a\": {\"b\": [1, 2, 3], \"c\": true}}',\n", + " None,\n", + "]\n", + "bpd.Series(json_data, dtype=pd.ArrowDtype(db_dtypes.JSONArrowType()))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 2: Create a Series with a Nested JSON dtype from local data**\n", + "\n", + "To create a BigQuery DataFrame Series containing `JSON` data nested within a `STRUCT` or `LIST` type, you must represent the `JSON` data in a `pa.array` defined with the `pa.string` type. This workaround is necessary because Pyarrow lacks support for creating structs or lists that directly contain extension types (see [issue](https://github.com/apache/arrow/issues/45262))." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 [{'key': '1'}]\n", + "1 [{'key': None}]\n", + "2 [{'key': '[\"1\",\"3\",\"5\"]'}]\n", + "3 [{'key': '{\"a\":1,\"b\":[\"x\",\"y\"],\"c\":{\"x\":[],\"z\"...\n", + "dtype: list>>>[pyarrow]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list_data = [\n", + " [{\"key\": \"1\"}],\n", + " [{\"key\": None}],\n", + " [{\"key\": '[\"1\",\"3\",\"5\"]'}],\n", + " [{\"key\": '{\"a\":1,\"b\":[\"x\",\"y\"],\"c\":{\"x\":[],\"z\":false}}'}],\n", + "]\n", + "pa_array = pa.array(list_data, type=pa.list_(pa.struct([(\"key\", pa.string())])))\n", + "bpd.Series(\n", + " pd.arrays.ArrowExtensionArray(pa_array),\n", + " dtype=pd.ArrowDtype(\n", + " pa.list_(pa.struct([(\"key\", db_dtypes.JSONArrowType())])),\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 3: Create a Series with a Nested JSON dtype using BigQuery SQLs**" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idstruct_col
01{'data': '{\"b\":100}', 'number': 2}
10{'data': '{\"a\":true}', 'number': 1}
\n", + "

2 rows × 2 columns

\n", + "
[2 rows x 2 columns in total]" + ], + "text/plain": [ + " id struct_col\n", + "0 1 {'data': '{\"b\":100}', 'number': 2}\n", + "1 0 {'data': '{\"a\":true}', 'number': 1}\n", + "\n", + "[2 rows x 2 columns]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sql = \"\"\"\n", + "SELECT 0 AS id, STRUCT(JSON_OBJECT('a', True) AS data, 1 AS number) AS struct_col\n", + "UNION ALL\n", + "SELECT 1, STRUCT(JSON_OBJECT('b', 100), 2),\n", + "\"\"\"\n", + "df = bpd.read_gbq(sql)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "id Int64\n", + "struct_col struct>,...\n", + "dtype: object" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Operate on `JSON` data\n", + "\n", + "The `bigframes.bigquery` module (often abbreviated as `bbq`) provides access within BigQuery DataFrames to various **[BigQuery built-in functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/json_functions)**. Examples relevant for JSON data include [`json_extract`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_json_extract) and [`parse_json`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_parse_json)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract JSON data via specific JSON path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 1: When JSON data is represented as strings**" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "fruits = [\n", + " '{\"fruits\": [{\"name\": \"apple\"}, {\"name\": \"cherry\"}]}',\n", + " '{\"fruits\": [{\"name\": \"guava\"}, {\"name\": \"grapes\"}]}',\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {\"fruits\": [{\"name\": \"apple\"}, {\"name\": \"cherr...\n", + "1 {\"fruits\": [{\"name\": \"guava\"}, {\"name\": \"grape...\n", + "dtype: string" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str_s = bpd.Series(fruits, dtype=\"string\")\n", + "str_s" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {\"name\":\"apple\"}\n", + "1 {\"name\":\"guava\"}\n", + "dtype: string" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.json_extract(str_s, \"$.fruits[0]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 2: When JSON data is stored as JSON type**" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {\"fruits\":[{\"name\":\"apple\"},{\"name\":\"cherry\"}]}\n", + "1 {\"fruits\":[{\"name\":\"guava\"},{\"name\":\"grapes\"}]}\n", + "dtype: extension>[pyarrow]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "json_s = bpd.Series(fruits, dtype=pd.ArrowDtype(db_dtypes.JSONArrowType()))\n", + "json_s" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {\"name\":\"apple\"}\n", + "1 {\"name\":\"guava\"}\n", + "dtype: extension>[pyarrow]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.json_extract(json_s, \"$.fruits[0]\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract an array from JSON data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 ['{\"name\":\"apple\"}' '{\"name\":\"cherry\"}']\n", + "1 ['{\"name\":\"guava\"}' '{\"name\":\"grapes\"}']\n", + "dtype: list>>[pyarrow]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.json_extract_array(json_s, \"$.fruits\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 ['{\"name\":\"apple\"}' '{\"name\":\"cherry\"}']\n", + "1 ['{\"name\":\"guava\"}' '{\"name\":\"grapes\"}']\n", + "dtype: list[pyarrow]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.json_extract_array(str_s, \"$.fruits\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/data_types/struct.ipynb b/notebooks/data_types/struct.ipynb new file mode 100644 index 0000000000..74bf69d239 --- /dev/null +++ b/notebooks/data_types/struct.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Struct Data Types\n", + "\n", + "In BigQuery, a [STRUCT](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct_type) (also known as a `record`) is a collection of ordered fields, each with a defined data type (required) and an optional field name. BigQuery DataFrames maps BigQuery `STRUCT` types to the pandas equivalent, `pandas.ArrowDtype(pa.struct())`. \n", + "\n", + "This notebook illustrates how to work with `STRUCT` columns in BigQuery DataFrames. First, let's import the required packages and perform the necessary setup below." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "import bigframes.bigquery as bbq\n", + "import pandas as pd\n", + "import pyarrow as pa" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}\n", + "\n", + "bpd.options.display.progress_bar = None\n", + "bpd.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create DataFrames with struct columns\n", + "\n", + "**Example 1: Creating from a list of objects**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameAddress
0Alice{'City': 'New York', 'State': 'NY'}
1Bob{'City': 'San Francisco', 'State': 'CA'}
2Charlie{'City': 'Seattle', 'State': 'WA'}
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " Name Address\n", + "0 Alice {'City': 'New York', 'State': 'NY'}\n", + "1 Bob {'City': 'San Francisco', 'State': 'CA'}\n", + "2 Charlie {'City': 'Seattle', 'State': 'WA'}\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names = [\"Alice\", \"Bob\", \"Charlie\"]\n", + "addresses = [\n", + " {'City': 'New York', 'State': 'NY'},\n", + " {'City': 'San Francisco', 'State': 'CA'},\n", + " {'City': 'Seattle', 'State': 'WA'}\n", + "]\n", + "df = bpd.DataFrame({'Name': names, 'Address': addresses})\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Name string[pyarrow]\n", + "Address struct[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 2: Defining schema explicitly**" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 {'City': 'New York', 'State': 'NY'}\n", + "1 {'City': 'San Francisco', 'State': 'CA'}\n", + "2 {'City': 'Seattle', 'State': 'WA'}\n", + "dtype: struct[pyarrow]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bpd.Series(\n", + " data=addresses, \n", + " dtype=bpd.ArrowDtype(pa.struct([('City', pa.string()), ('State', pa.string())]))\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 3: Reading from a source**" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 [{'tables': {'score': 0.9349926710128784, 'val...\n", + "1 [{'tables': {'score': 0.9690881371498108, 'val...\n", + "2 [{'tables': {'score': 0.8667634129524231, 'val...\n", + "3 [{'tables': {'score': 0.9351968765258789, 'val...\n", + "4 [{'tables': {'score': 0.8572560548782349, 'val...\n", + "Name: predicted_default_payment_next_month, dtype: list>>[pyarrow]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bpd.read_gbq(\"bigquery-public-data.ml_datasets.credit_card_default\", max_results=5)[\"predicted_default_payment_next_month\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Operate on `STRUCT` data\n", + "\n", + "BigQuery DataFrames provides two main approaches for operating on `STRUCT` data:\n", + "\n", + "1. **[The `Series.struct` accessor](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.operations.structs.StructAccessor)**: Provides Pandas-like methods for STRUCT column manipulation.\n", + "2. **The `DataFrame.struct` accessor**: Provides Pandas-like methods for all child STRUCT columns manipulation.\n", + "3. **[BigQuery built-in functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions)**: Allows you to use functions mirroring BigQuery SQL operations, available through the `bigframes.bigquery` module (abbreviated as `bbq` below), such as [`struct`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_struct)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### View Data Types of Struct Fields" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "City string[pyarrow]\n", + "State string[pyarrow]\n", + "dtype: object" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['Address'].struct.dtypes()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Access a Struct Field by Name" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 New York\n", + "1 San Francisco\n", + "2 Seattle\n", + "Name: City, dtype: string" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['Address'].struct.field(\"City\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract Struct Fields into a DataFrame\n", + "\n", + "**Example 1: Using Series `.struct` accessor**" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CityState
0New YorkNY
1San FranciscoCA
2SeattleWA
\n", + "

3 rows × 2 columns

\n", + "
[3 rows x 2 columns in total]" + ], + "text/plain": [ + " City State\n", + "0 New York NY\n", + "1 San Francisco CA\n", + "2 Seattle WA\n", + "\n", + "[3 rows x 2 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['Address'].struct.explode()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example 2: Using DataFrame `.struct` accessor while keeping other columns**" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameAddress.CityAddress.State
0AliceNew YorkNY
1BobSan FranciscoCA
2CharlieSeattleWA
\n", + "

3 rows × 3 columns

\n", + "
[3 rows x 3 columns in total]" + ], + "text/plain": [ + " Name Address.City Address.State\n", + "0 Alice New York NY\n", + "1 Bob San Francisco CA\n", + "2 Charlie Seattle WA\n", + "\n", + "[3 rows x 3 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.struct.explode(\"Address\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 8482bfc1d4caa91a35c4fbf0be420301d05ad544 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 24 Apr 2025 14:11:49 -0700 Subject: [PATCH 11/28] perf: Use flyweight for node fields (#1654) --- bigframes/core/bigframe_node.py | 5 +- bigframes/core/nodes.py | 80 ++++++++++++++---------- bigframes/core/schema.py | 7 ++- bigframes/core/sequences.py | 105 ++++++++++++++++++++++++++++++++ tests/unit/test_sequences.py | 55 +++++++++++++++++ 5 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 bigframes/core/sequences.py create mode 100644 tests/unit/test_sequences.py diff --git a/bigframes/core/bigframe_node.py b/bigframes/core/bigframe_node.py index 369e8f6329..5509adc0ea 100644 --- a/bigframes/core/bigframe_node.py +++ b/bigframes/core/bigframe_node.py @@ -20,10 +20,9 @@ import functools import itertools import typing -from typing import Callable, Dict, Generator, Iterable, Mapping, Set, Tuple +from typing import Callable, Dict, Generator, Iterable, Mapping, Sequence, Set, Tuple from bigframes.core import identifiers -import bigframes.core.guid import bigframes.core.schema as schemata import bigframes.dtypes @@ -163,7 +162,7 @@ def roots(self) -> typing.Set[BigFrameNode]: # TODO: Store some local data lazily for select, aggregate nodes. @property @abc.abstractmethod - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: ... @property diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index d2c301b4ad..f7327f2a7a 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -33,7 +33,7 @@ import google.cloud.bigquery as bq -from bigframes.core import identifiers, local_data +from bigframes.core import identifiers, local_data, sequences from bigframes.core.bigframe_node import BigFrameNode, COLUMN_SET, Field import bigframes.core.expression as ex from bigframes.core.ordering import OrderingExpression, RowOrdering @@ -87,7 +87,7 @@ def child_nodes(self) -> typing.Sequence[BigFrameNode]: return (self.child,) @property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: return self.child.fields @property @@ -226,8 +226,8 @@ def added_fields(self) -> Tuple[Field, ...]: return (Field(self.indicator_col, bigframes.dtypes.BOOL_DTYPE, nullable=False),) @property - def fields(self) -> Iterable[Field]: - return itertools.chain( + def fields(self) -> Sequence[Field]: + return sequences.ChainedSequence( self.left_child.fields, self.added_fields, ) @@ -321,15 +321,15 @@ def order_ambiguous(self) -> bool: def explicitly_ordered(self) -> bool: return self.propogate_order - @property - def fields(self) -> Iterable[Field]: - left_fields = self.left_child.fields + @functools.cached_property + def fields(self) -> Sequence[Field]: + left_fields: Iterable[Field] = self.left_child.fields if self.type in ("right", "outer"): left_fields = map(lambda x: x.with_nullable(), left_fields) - right_fields = self.right_child.fields + right_fields: Iterable[Field] = self.right_child.fields if self.type in ("left", "outer"): right_fields = map(lambda x: x.with_nullable(), right_fields) - return itertools.chain(left_fields, right_fields) + return (*left_fields, *right_fields) @property def joins_nulls(self) -> bool: @@ -430,10 +430,10 @@ def explicitly_ordered(self) -> bool: return True @property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: # TODO: Output names should probably be aligned beforehand or be part of concat definition # TODO: Handle nullability - return ( + return tuple( Field(id, field.dtype) for id, field in zip(self.output_ids, self.children[0].fields) ) @@ -505,7 +505,7 @@ def explicitly_ordered(self) -> bool: return True @functools.cached_property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: return ( Field(self.output_id, next(iter(self.start.fields)).dtype, nullable=False), ) @@ -626,12 +626,20 @@ class ReadLocalNode(LeafNode): session: typing.Optional[bigframes.session.Session] = None @property - def fields(self) -> Iterable[Field]: - fields = (Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items) + def fields(self) -> Sequence[Field]: + fields = tuple( + Field(col_id, dtype) for col_id, dtype, _ in self.scan_list.items + ) if self.offsets_col is not None: - return itertools.chain( - fields, - (Field(self.offsets_col, bigframes.dtypes.INT_DTYPE, nullable=False),), + return tuple( + itertools.chain( + fields, + ( + Field( + self.offsets_col, bigframes.dtypes.INT_DTYPE, nullable=False + ), + ), + ) ) return fields @@ -767,8 +775,8 @@ def session(self): return self.table_session @property - def fields(self) -> Iterable[Field]: - return ( + def fields(self) -> Sequence[Field]: + return tuple( Field(col_id, dtype, self.source.table.schema_by_id[source_id].is_nullable) for col_id, dtype, source_id in self.scan_list.items ) @@ -881,8 +889,8 @@ def non_local(self) -> bool: return True @property - def fields(self) -> Iterable[Field]: - return itertools.chain(self.child.fields, self.added_fields) + def fields(self) -> Sequence[Field]: + return sequences.ChainedSequence(self.child.fields, self.added_fields) @property def relation_ops_created(self) -> int: @@ -1097,7 +1105,7 @@ def _validate(self): raise ValueError(f"Reference to column not in child: {ref.id}") @functools.cached_property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: input_fields_by_id = {field.id: field for field in self.child.fields} return tuple( Field( @@ -1192,8 +1200,8 @@ def added_fields(self) -> Tuple[Field, ...]: return tuple(fields) @property - def fields(self) -> Iterable[Field]: - return itertools.chain(self.child.fields, self.added_fields) + def fields(self) -> Sequence[Field]: + return sequences.ChainedSequence(self.child.fields, self.added_fields) @property def variables_introduced(self) -> int: @@ -1263,7 +1271,7 @@ def non_local(self) -> bool: return True @property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: return (Field(self.col_id, bigframes.dtypes.INT_DTYPE, nullable=False),) @property @@ -1313,7 +1321,7 @@ def non_local(self) -> bool: return True @functools.cached_property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: # TODO: Use child nullability to infer grouping key nullability by_fields = (self.child.field_by_id[ref.id] for ref in self.by_column_ids) if self.dropna: @@ -1411,8 +1419,8 @@ def non_local(self) -> bool: return True @property - def fields(self) -> Iterable[Field]: - return itertools.chain(self.child.fields, [self.added_field]) + def fields(self) -> Sequence[Field]: + return sequences.ChainedSequence(self.child.fields, (self.added_field,)) @property def variables_introduced(self) -> int: @@ -1547,7 +1555,7 @@ def row_preserving(self) -> bool: return False @property - def fields(self) -> Iterable[Field]: + def fields(self) -> Sequence[Field]: fields = ( Field( field.id, @@ -1561,11 +1569,17 @@ def fields(self) -> Iterable[Field]: for field in self.child.fields ) if self.offsets_col is not None: - return itertools.chain( - fields, - (Field(self.offsets_col, bigframes.dtypes.INT_DTYPE, nullable=False),), + return tuple( + itertools.chain( + fields, + ( + Field( + self.offsets_col, bigframes.dtypes.INT_DTYPE, nullable=False + ), + ), + ) ) - return fields + return tuple(fields) @property def relation_ops_created(self) -> int: diff --git a/bigframes/core/schema.py b/bigframes/core/schema.py index c4cbb51ef9..4f636ab210 100644 --- a/bigframes/core/schema.py +++ b/bigframes/core/schema.py @@ -17,11 +17,11 @@ from dataclasses import dataclass import functools import typing +from typing import Sequence import google.cloud.bigquery import pyarrow -import bigframes.core.guid import bigframes.dtypes ColumnIdentifierType = str @@ -35,7 +35,10 @@ class SchemaItem: @dataclass(frozen=True) class ArraySchema: - items: typing.Tuple[SchemaItem, ...] + items: Sequence[SchemaItem] + + def __iter__(self): + yield from self.items @classmethod def from_bq_table( diff --git a/bigframes/core/sequences.py b/bigframes/core/sequences.py new file mode 100644 index 0000000000..6f1b7e455b --- /dev/null +++ b/bigframes/core/sequences.py @@ -0,0 +1,105 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections.abc +import functools +import itertools +from typing import Iterable, Iterator, Sequence, TypeVar + +ColumnIdentifierType = str + + +T = TypeVar("T") + +# Further optimizations possible: +# * Support mapping operators +# * Support insertions and deletions + + +class ChainedSequence(collections.abc.Sequence[T]): + """ + Memory-optimized sequence from composing chain of existing sequences. + + Will use the provided parts as underlying storage - so do not mutate provided parts. + May merge small underlying parts for better access performance. + """ + + def __init__(self, *parts: Sequence[T]): + # Could build an index that makes random access faster? + self._parts: tuple[Sequence[T], ...] = tuple( + _defrag_parts(_flatten_parts(parts)) + ) + + def __getitem__(self, index): + if isinstance(index, slice): + return tuple(self)[index] + if index < 0: + index = len(self) + index + if index < 0: + raise IndexError("Index out of bounds") + + offset = 0 + for part in self._parts: + if (index - offset) < len(part): + return part[index - offset] + offset += len(part) + raise IndexError("Index out of bounds") + + @functools.cache + def __len__(self): + return sum(map(len, self._parts)) + + def __iter__(self): + for part in self._parts: + yield from part + + +def _flatten_parts(parts: Iterable[Sequence[T]]) -> Iterator[Sequence[T]]: + for part in parts: + if isinstance(part, ChainedSequence): + yield from part._parts + else: + yield part + + +# Should be a cache-friendly chunk size? +_TARGET_SIZE = 128 +_MAX_MERGABLE = 32 + + +def _defrag_parts(parts: Iterable[Sequence[T]]) -> Iterator[Sequence[T]]: + """ + Merge small chunks into larger chunks for better performance. + """ + parts_queue: list[Sequence[T]] = [] + queued_items = 0 + for part in parts: + # too big, just yield from the buffer + if len(part) > _MAX_MERGABLE: + yield from parts_queue + parts_queue = [] + queued_items = 0 + yield part + else: # can be merged, so lets add to the queue + parts_queue.append(part) + queued_items += len(part) + # if queue has reached target size, merge, dump and reset queue + if queued_items >= _TARGET_SIZE: + yield tuple(itertools.chain(*parts_queue)) + parts_queue = [] + queued_items = 0 + + yield from parts_queue diff --git a/tests/unit/test_sequences.py b/tests/unit/test_sequences.py new file mode 100644 index 0000000000..d901670b9b --- /dev/null +++ b/tests/unit/test_sequences.py @@ -0,0 +1,55 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import itertools +from typing import Sequence + +import pytest + +from bigframes.core import sequences + +LARGE_LIST = list(range(100, 500)) +SMALL_LIST = list(range(1, 5)) +CHAINED_LIST = sequences.ChainedSequence([SMALL_LIST for i in range(100)]) + + +def _build_reference(*parts): + return tuple(itertools.chain(*parts)) + + +def _check_equivalence(expected: Sequence, actual: Sequence): + assert len(expected) == len(actual) + assert tuple(expected) == tuple(actual) + assert expected[10:1:-2] == actual[10:1:-2] + if len(expected) > 0: + assert expected[len(expected) - 1] == expected[len(actual) - 1] + + +@pytest.mark.parametrize( + ("parts",), + [ + ([],), + ([[]],), + ([[0, 1, 2]],), + ([LARGE_LIST, SMALL_LIST, LARGE_LIST],), + ([SMALL_LIST * 100],), + ([CHAINED_LIST, LARGE_LIST, CHAINED_LIST, SMALL_LIST],), + ], +) +def test_init_chained_sequence_single_slist(parts): + value = sequences.ChainedSequence(*parts) + expected = _build_reference(*parts) + _check_equivalence(expected, value) From cd7fbde026522f53a23a4bb6585ad8629769fad1 Mon Sep 17 00:00:00 2001 From: jialuoo Date: Thu, 24 Apr 2025 14:49:28 -0700 Subject: [PATCH 12/28] fix: resolve some of the typo errors (#1655) * fix: resolve the type in geo.py * fix --- bigframes/bigquery/_operations/geo.py | 2 +- bigframes/core/compile/googlesql/expression.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bigframes/bigquery/_operations/geo.py b/bigframes/bigquery/_operations/geo.py index 0856baabea..fc9bd1a653 100644 --- a/bigframes/bigquery/_operations/geo.py +++ b/bigframes/bigquery/_operations/geo.py @@ -264,7 +264,7 @@ def st_distance( A series containing geography objects. other (bigframes.pandas.Series | bigframes.geopandas.GeoSeries | shapely.Geometry): The series or geometric object to calculate the distance in meters - to from the geography objects in ``series``. + to form the geography objects in ``series``. use_spheroid (optional, default ``False``): Determines how this function measures distance. If ``use_spheroid`` is False, the function measures distance on the surface of a perfect diff --git a/bigframes/core/compile/googlesql/expression.py b/bigframes/core/compile/googlesql/expression.py index 20d6dbf9a1..581ab67718 100644 --- a/bigframes/core/compile/googlesql/expression.py +++ b/bigframes/core/compile/googlesql/expression.py @@ -25,7 +25,7 @@ * `expression`: Models basic SQL expressions. Extended classes (not part of standard GoogleSQL syntax, but added for convenience): -i + * `ColumnExpression`: Represents column references. * `TableExpression`: Represents table references. * `AliasExpression`: Represents aliased expressions. From 6199023a6a71e72e926f5879e74a15215bc6e4a0 Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:14:28 -0700 Subject: [PATCH 13/28] feat: add GeminiTextGenerator.predict structured output (#1653) * feat: add GeminiTextGenerator.predict structured output * test * fix tests --- bigframes/ml/base.py | 22 +-- bigframes/ml/core.py | 31 +++- bigframes/ml/globals.py | 2 +- bigframes/ml/imported.py | 35 ++-- bigframes/ml/llm.py | 111 +++++------ bigframes/ml/remote.py | 21 +-- bigframes/ml/sql.py | 40 ++-- bigframes/ml/utils.py | 25 ++- tests/system/small/ml/test_llm.py | 296 ++++++++++++++++++------------ 9 files changed, 339 insertions(+), 244 deletions(-) diff --git a/bigframes/ml/base.py b/bigframes/ml/base.py index 2b25bc82f0..c36457d0b5 100644 --- a/bigframes/ml/base.py +++ b/bigframes/ml/base.py @@ -22,7 +22,7 @@ """ import abc -from typing import Callable, cast, Mapping, Optional, TypeVar, Union +from typing import cast, Optional, TypeVar, Union import warnings import bigframes_vendored.sklearn.base @@ -244,18 +244,12 @@ def fit( class RetriableRemotePredictor(BaseEstimator): - @property - @abc.abstractmethod - def _predict_func(self) -> Callable[[bpd.DataFrame, Mapping], bpd.DataFrame]: - pass - - @property - @abc.abstractmethod - def _status_col(self) -> str: - pass - def _predict_and_retry( - self, X: bpd.DataFrame, options: Mapping, max_retries: int + self, + bqml_model_predict_tvf: core.BqmlModel.TvfDef, + X: bpd.DataFrame, + options: dict, + max_retries: int, ) -> bpd.DataFrame: assert self._bqml_model is not None @@ -269,9 +263,9 @@ def _predict_and_retry( warnings.warn(msg, category=RuntimeWarning) break - df = self._predict_func(df_fail, options) + df = bqml_model_predict_tvf.tvf(self._bqml_model, df_fail, options) - success = df[self._status_col].str.len() == 0 + success = df[bqml_model_predict_tvf.status_col].str.len() == 0 df_succ = df[success] df_fail = df[~success] diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index 81637333b0..e11f7d82ba 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -16,6 +16,7 @@ from __future__ import annotations +import dataclasses import datetime from typing import Callable, cast, Iterable, Mapping, Optional, Union import uuid @@ -44,6 +45,11 @@ class BqmlModel(BaseBqml): BigQuery DataFrames ML. """ + @dataclasses.dataclass + class TvfDef: + tvf: Callable[[BqmlModel, bpd.DataFrame, dict], bpd.DataFrame] + status_col: str + def __init__(self, session: bigframes.Session, model: bigquery.Model): self._session = session self._model = model @@ -159,8 +165,9 @@ def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: def generate_text( self, input_data: bpd.DataFrame, - options: Mapping[str, int | float], + options: dict[str, Union[int, float, bool]], ) -> bpd.DataFrame: + options["flatten_json_output"] = True return self._apply_ml_tvf( input_data, lambda source_sql: self._model_manipulation_sql_generator.ml_generate_text( @@ -169,11 +176,14 @@ def generate_text( ), ) + generate_text_tvf = TvfDef(generate_text, "ml_generate_text_status") + def generate_embedding( self, input_data: bpd.DataFrame, - options: Mapping[str, int | float], + options: dict[str, Union[int, float, bool]], ) -> bpd.DataFrame: + options["flatten_json_output"] = True return self._apply_ml_tvf( input_data, lambda source_sql: self._model_manipulation_sql_generator.ml_generate_embedding( @@ -182,6 +192,23 @@ def generate_embedding( ), ) + generate_embedding_tvf = TvfDef(generate_embedding, "ml_generate_embedding_status") + + def generate_table( + self, + input_data: bpd.DataFrame, + options: dict[str, Union[int, float, bool, Mapping]], + ) -> bpd.DataFrame: + return self._apply_ml_tvf( + input_data, + lambda source_sql: self._model_manipulation_sql_generator.ai_generate_table( + source_sql=source_sql, + struct_options=options, + ), + ) + + generate_table_tvf = TvfDef(generate_table, "status") + def detect_anomalies( self, input_data: bpd.DataFrame, options: Mapping[str, int | float] ) -> bpd.DataFrame: diff --git a/bigframes/ml/globals.py b/bigframes/ml/globals.py index 44e9463727..62cfdbef72 100644 --- a/bigframes/ml/globals.py +++ b/bigframes/ml/globals.py @@ -19,7 +19,7 @@ _BASE_SQL_GENERATOR = sql.BaseSqlGenerator() _BQML_MODEL_FACTORY = core.BqmlModelFactory() -_SUPPORTED_DTYPES = ( +_REMOTE_MODEL_SUPPORTED_DTYPES = ( "bool", "string", "int64", diff --git a/bigframes/ml/imported.py b/bigframes/ml/imported.py index 93152a6b99..a73ee352d0 100644 --- a/bigframes/ml/imported.py +++ b/bigframes/ml/imported.py @@ -216,8 +216,8 @@ def __init__( self, model_path: str, *, - input: Mapping[str, str] = {}, - output: Mapping[str, str] = {}, + input: Optional[Mapping[str, str]] = None, + output: Optional[Mapping[str, str]] = None, session: Optional[bigframes.session.Session] = None, ): self.session = session or bpd.get_global_session() @@ -234,20 +234,23 @@ def _create_bqml_model(self): return self._bqml_model_factory.create_imported_model( session=self.session, options=options ) - else: - for io in (self.input, self.output): - for v in io.values(): - if v not in globals._SUPPORTED_DTYPES: - raise ValueError( - f"field_type {v} is not supported. We only support {', '.join(globals._SUPPORTED_DTYPES)}." - ) - - return self._bqml_model_factory.create_xgboost_imported_model( - session=self.session, - input=self.input, - output=self.output, - options=options, - ) + if not self.input or not self.output: + raise ValueError("input and output must both or neigher be set.") + self.input = { + k: utils.standardize_type(v, globals._REMOTE_MODEL_SUPPORTED_DTYPES) + for k, v in self.input.items() + } + self.output = { + k: utils.standardize_type(v, globals._REMOTE_MODEL_SUPPORTED_DTYPES) + for k, v in self.output.items() + } + + return self._bqml_model_factory.create_xgboost_imported_model( + session=self.session, + input=self.input, + output=self.output, + options=options, + ) @classmethod def _from_bq( diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index cce05ea1f2..49dbfd3443 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -16,7 +16,7 @@ from __future__ import annotations -from typing import Callable, cast, Iterable, Literal, Mapping, Optional, Union +from typing import cast, Iterable, Literal, Mapping, Optional, Union import warnings import bigframes_vendored.constants as constants @@ -92,10 +92,6 @@ _CLAUDE_3_OPUS_ENDPOINT, ) - -_ML_GENERATE_TEXT_STATUS = "ml_generate_text_status" -_ML_GENERATE_EMBEDDING_STATUS = "ml_generate_embedding_status" - _MODEL_NOT_SUPPORTED_WARNING = ( "Model name '{model_name}' is not supported. " "We are currently aware of the following models: {known_models}. " @@ -193,18 +189,6 @@ def _from_bq( model._bqml_model = core.BqmlModel(session, bq_model) return model - @property - def _predict_func( - self, - ) -> Callable[ - [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame - ]: - return self._bqml_model.generate_embedding - - @property - def _status_col(self) -> str: - return _ML_GENERATE_EMBEDDING_STATUS - def predict( self, X: utils.ArrayType, *, max_retries: int = 0 ) -> bigframes.dataframe.DataFrame: @@ -233,11 +217,14 @@ def predict( col_label = cast(blocks.Label, X.columns[0]) X = X.rename(columns={col_label: "content"}) - options = { - "flatten_json_output": True, - } + options: dict = {} - return self._predict_and_retry(X, options=options, max_retries=max_retries) + return self._predict_and_retry( + core.BqmlModel.generate_embedding_tvf, + X, + options=options, + max_retries=max_retries, + ) def to_gbq(self, model_name: str, replace: bool = False) -> TextEmbeddingGenerator: """Save the model to BigQuery. @@ -339,18 +326,6 @@ def _from_bq( model._bqml_model = core.BqmlModel(session, bq_model) return model - @property - def _predict_func( - self, - ) -> Callable[ - [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame - ]: - return self._bqml_model.generate_embedding - - @property - def _status_col(self) -> str: - return _ML_GENERATE_EMBEDDING_STATUS - def predict( self, X: utils.ArrayType, *, max_retries: int = 0 ) -> bigframes.dataframe.DataFrame: @@ -384,11 +359,14 @@ def predict( if X["content"].dtype == dtypes.OBJ_REF_DTYPE: X["content"] = X["content"].blob._get_runtime("R", with_metadata=True) - options = { - "flatten_json_output": True, - } + options: dict = {} - return self._predict_and_retry(X, options=options, max_retries=max_retries) + return self._predict_and_retry( + core.BqmlModel.generate_embedding_tvf, + X, + options=options, + max_retries=max_retries, + ) def to_gbq( self, model_name: str, replace: bool = False @@ -533,18 +511,6 @@ def _bqml_options(self) -> dict: } return options - @property - def _predict_func( - self, - ) -> Callable[ - [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame - ]: - return self._bqml_model.generate_text - - @property - def _status_col(self) -> str: - return _ML_GENERATE_TEXT_STATUS - def fit( self, X: utils.ArrayType, @@ -596,6 +562,7 @@ def predict( ground_with_google_search: bool = False, max_retries: int = 0, prompt: Optional[Iterable[Union[str, bigframes.series.Series]]] = None, + output_schema: Optional[Mapping[str, str]] = None, ) -> bigframes.dataframe.DataFrame: """Predict the result from input DataFrame. @@ -645,6 +612,9 @@ def predict( Construct a prompt struct column for prediction based on the input. The input must be an Iterable that can take string literals, such as "summarize", string column(s) of X, such as X["str_col"], or blob column(s) of X, such as X["blob_col"]. It creates a struct column of the items of the iterable, and use the concatenated result as the input prompt. No-op if set to None. + output_schema (Mapping[str, str] or None, default None): + The schema used to generate structured output as a bigframes DataFrame. The schema is a string key-value pair of :. + Supported types are int64, float64, bool and string. If None, output text result. Returns: bigframes.dataframe.DataFrame: DataFrame of shape (n_samples, n_input_columns + n_prediction_columns). Returns predicted values. """ @@ -707,16 +677,31 @@ def predict( col_label = cast(blocks.Label, X.columns[0]) X = X.rename(columns={col_label: "prompt"}) - options = { + options: dict = { "temperature": temperature, "max_output_tokens": max_output_tokens, - "top_k": top_k, + # "top_k": top_k, # TODO(garrettwu): the option is deprecated in Gemini 1.5 forward. "top_p": top_p, - "flatten_json_output": True, "ground_with_google_search": ground_with_google_search, } + if output_schema: + output_schema = { + k: utils.standardize_type(v) for k, v in output_schema.items() + } + options["output_schema"] = output_schema + return self._predict_and_retry( + core.BqmlModel.generate_table_tvf, + X, + options=options, + max_retries=max_retries, + ) - return self._predict_and_retry(X, options=options, max_retries=max_retries) + return self._predict_and_retry( + core.BqmlModel.generate_text_tvf, + X, + options=options, + max_retries=max_retries, + ) def score( self, @@ -916,18 +901,6 @@ def _bqml_options(self) -> dict: } return options - @property - def _predict_func( - self, - ) -> Callable[ - [bigframes.dataframe.DataFrame, Mapping], bigframes.dataframe.DataFrame - ]: - return self._bqml_model.generate_text - - @property - def _status_col(self) -> str: - return _ML_GENERATE_TEXT_STATUS - def predict( self, X: utils.ArrayType, @@ -1000,10 +973,14 @@ def predict( "max_output_tokens": max_output_tokens, "top_k": top_k, "top_p": top_p, - "flatten_json_output": True, } - return self._predict_and_retry(X, options=options, max_retries=max_retries) + return self._predict_and_retry( + core.BqmlModel.generate_text_tvf, + X, + options=options, + max_retries=max_retries, + ) def to_gbq(self, model_name: str, replace: bool = False) -> Claude3TextGenerator: """Save the model to BigQuery. diff --git a/bigframes/ml/remote.py b/bigframes/ml/remote.py index cc711cbe3b..b091c61f3f 100644 --- a/bigframes/ml/remote.py +++ b/bigframes/ml/remote.py @@ -78,19 +78,14 @@ def _create_bqml_model(self): "endpoint": self.endpoint, } - def standardize_type(v: str): - v = v.lower() - v = v.replace("boolean", "bool") - - if v not in globals._SUPPORTED_DTYPES: - raise ValueError( - f"Data type {v} is not supported. We only support {', '.join(globals._SUPPORTED_DTYPES)}." - ) - - return v - - self.input = {k: standardize_type(v) for k, v in self.input.items()} - self.output = {k: standardize_type(v) for k, v in self.output.items()} + self.input = { + k: utils.standardize_type(v, globals._REMOTE_MODEL_SUPPORTED_DTYPES) + for k, v in self.input.items() + } + self.output = { + k: utils.standardize_type(v, globals._REMOTE_MODEL_SUPPORTED_DTYPES) + for k, v in self.output.items() + } return self._bqml_model_factory.create_remote_model( session=self.session, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index a756fac3b9..84ea37c5fc 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -24,6 +24,8 @@ import bigframes.core.compile.googlesql as sql_utils import bigframes.core.sql as sql_vals +INDENT_STR = " " + # TODO: Add proper escaping logic from core/compile module class BaseSqlGenerator: @@ -44,35 +46,35 @@ def encode_value(self, v: Union[str, int, float, Iterable[str]]) -> str: def build_parameters(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: """Encode a dict of values into a formatted Iterable of key-value pairs for SQL""" - indent_str = " " param_strs = [f"{k}={self.encode_value(v)}" for k, v in kwargs.items()] - return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(param_strs) - def build_structs(self, **kwargs: Union[int, float]) -> str: + def build_structs(self, **kwargs: Union[int, float, str, Mapping]) -> str: """Encode a dict of values into a formatted STRUCT items for SQL""" - indent_str = " " - param_strs = [ - f"{sql_vals.simple_literal(v)} AS {sql_utils.identifier(k)}" - for k, v in kwargs.items() - ] - return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + param_strs = [] + for k, v in kwargs.items(): + v_trans = self.build_schema(**v) if isinstance(v, Mapping) else v + + param_strs.append( + f"{sql_vals.simple_literal(v_trans)} AS {sql_utils.identifier(k)}" + ) + + return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(param_strs) def build_expressions(self, *expr_sqls: str) -> str: """Encode a Iterable of SQL expressions into a formatted Iterable for SQL""" - indent_str = " " - return "\n" + indent_str + f",\n{indent_str}".join(expr_sqls) + return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(expr_sqls) def build_schema(self, **kwargs: str) -> str: """Encode a dict of values into a formatted schema type items for SQL""" - indent_str = " " param_strs = [f"{sql_utils.identifier(k)} {v}" for k, v in kwargs.items()] - return "\n" + indent_str + f",\n{indent_str}".join(param_strs) + return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(param_strs) def options(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: """Encode the OPTIONS clause for BQML""" return f"OPTIONS({self.build_parameters(**kwargs)})" - def struct_options(self, **kwargs: Union[int, float]) -> str: + def struct_options(self, **kwargs: Union[int, float, Mapping]) -> str: """Encode a BQ STRUCT as options.""" return f"STRUCT({self.build_structs(**kwargs)})" @@ -406,3 +408,13 @@ def ml_transform(self, source_sql: str) -> str: """Encode ML.TRANSFORM for BQML""" return f"""SELECT * FROM ML.TRANSFORM(MODEL {self._model_ref_sql()}, ({source_sql}))""" + + def ai_generate_table( + self, + source_sql: str, + struct_options: Mapping[str, Union[int, float, bool, Mapping]], + ) -> str: + """Encode AI.GENERATE_TABLE for BQML""" + struct_options_sql = self.struct_options(**struct_options) + return f"""SELECT * FROM AI.GENERATE_TABLE(MODEL {self._model_ref_sql()}, + ({source_sql}), {struct_options_sql})""" diff --git a/bigframes/ml/utils.py b/bigframes/ml/utils.py index e034fd00f7..5c02789576 100644 --- a/bigframes/ml/utils.py +++ b/bigframes/ml/utils.py @@ -13,7 +13,17 @@ # limitations under the License. import typing -from typing import Any, Generator, Hashable, Literal, Mapping, Optional, Tuple, Union +from typing import ( + Any, + Generator, + Hashable, + Iterable, + Literal, + Mapping, + Optional, + Tuple, + Union, +) import bigframes_vendored.constants as constants from google.cloud import bigquery @@ -178,3 +188,16 @@ def combine_training_and_evaluation_data( bqml_options["data_split_col"] = split_col return X, y, bqml_options + + +def standardize_type(v: str, supported_dtypes: Optional[Iterable[str]] = None): + t = v.lower() + t = t.replace("boolean", "bool") + + if supported_dtypes: + if t not in supported_dtypes: + raise ValueError( + f"Data type {v} is not supported. We only support {', '.join(supported_dtypes)}." + ) + + return t diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 544889bf5a..90483667d8 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Callable from unittest import mock import pandas as pd @@ -222,6 +223,47 @@ def test_gemini_text_generator_multi_cols_predict_success( ) +@pytest.mark.parametrize( + "model_name", + ( + "gemini-1.5-pro-preview-0514", + "gemini-1.5-flash-preview-0514", + "gemini-1.5-pro-001", + "gemini-1.5-pro-002", + "gemini-1.5-flash-001", + "gemini-1.5-flash-002", + "gemini-2.0-flash-exp", + ), +) +@pytest.mark.flaky(retries=2) +def test_gemini_text_generator_predict_output_schema_success( + llm_text_df: bpd.DataFrame, model_name, session, bq_connection +): + gemini_text_generator_model = llm.GeminiTextGenerator( + model_name=model_name, connection_name=bq_connection, session=session + ) + output_schema = { + "bool_output": "bool", + "int_output": "int64", + "float_output": "float64", + "str_output": "string", + } + df = gemini_text_generator_model.predict( + llm_text_df, output_schema=output_schema + ).to_pandas() + utils.check_pandas_df_schema_and_index( + df, + columns=list(output_schema.keys()) + ["prompt", "full_response", "status"], + index=3, + col_exact=False, + ) + + assert df["bool_output"].dtype == pd.BooleanDtype() + assert df["int_output"].dtype == pd.Int64Dtype() + assert df["float_output"].dtype == pd.Float64Dtype() + assert df["str_output"].dtype == pd.StringDtype(storage="pyarrow") + + # Overrides __eq__ function for comparing as mock.call parameter class EqCmpAllDataFrame(bpd.DataFrame): def __eq__(self, other): @@ -239,9 +281,7 @@ def __eq__(self, other): { "temperature": 0.9, "max_output_tokens": 8192, - "top_k": 40, "top_p": 1.0, - "flatten_json_output": True, "ground_with_google_search": False, }, ), @@ -251,7 +291,6 @@ def __eq__(self, other): "max_output_tokens": 128, "top_k": 40, "top_p": 0.95, - "flatten_json_output": True, }, ), ], @@ -297,11 +336,16 @@ def test_text_generator_retry_success( session=session, ) + mock_generate_text = mock.create_autospec( + Callable[[core.BqmlModel, bpd.DataFrame, dict], bpd.DataFrame] + ) mock_bqml_model = mock.create_autospec(spec=core.BqmlModel) type(mock_bqml_model).session = mock.PropertyMock(return_value=session) - + generate_text_tvf = core.BqmlModel.TvfDef( + mock_generate_text, "ml_generate_text_status" + ) # Responses. Retry twice then all succeeded. - mock_bqml_model.generate_text.side_effect = [ + mock_generate_text.side_effect = [ EqCmpAllDataFrame( { "ml_generate_text_status": ["", "error", "error"], @@ -344,32 +388,33 @@ def test_text_generator_retry_success( ) text_generator_model._bqml_model = mock_bqml_model - # 3rd retry isn't triggered - result = text_generator_model.predict(df0, max_retries=3) + with mock.patch.object(core.BqmlModel, "generate_text_tvf", generate_text_tvf): + # 3rd retry isn't triggered + result = text_generator_model.predict(df0, max_retries=3) - mock_bqml_model.generate_text.assert_has_calls( - [ - mock.call(df0, options), - mock.call(df1, options), - mock.call(df2, options), - ] - ) - pd.testing.assert_frame_equal( - result.to_pandas(), - pd.DataFrame( - { - "ml_generate_text_status": ["", "", ""], - "prompt": [ - "What is BigQuery?", - "What is BigQuery DataFrame?", - "What is BQML?", - ], - }, - index=[0, 2, 1], - ), - check_dtype=False, - check_index_type=False, - ) + mock_generate_text.assert_has_calls( + [ + mock.call(mock_bqml_model, df0, options), + mock.call(mock_bqml_model, df1, options), + mock.call(mock_bqml_model, df2, options), + ] + ) + pd.testing.assert_frame_equal( + result.to_pandas(), + pd.DataFrame( + { + "ml_generate_text_status": ["", "", ""], + "prompt": [ + "What is BigQuery?", + "What is BigQuery DataFrame?", + "What is BQML?", + ], + }, + index=[0, 2, 1], + ), + check_dtype=False, + check_index_type=False, + ) @pytest.mark.parametrize( @@ -383,9 +428,7 @@ def test_text_generator_retry_success( { "temperature": 0.9, "max_output_tokens": 8192, - "top_k": 40, "top_p": 1.0, - "flatten_json_output": True, "ground_with_google_search": False, }, ), @@ -395,7 +438,6 @@ def test_text_generator_retry_success( "max_output_tokens": 128, "top_k": 40, "top_p": 0.95, - "flatten_json_output": True, }, ), ], @@ -431,10 +473,16 @@ def test_text_generator_retry_no_progress( session=session, ) + mock_generate_text = mock.create_autospec( + Callable[[core.BqmlModel, bpd.DataFrame, dict], bpd.DataFrame] + ) mock_bqml_model = mock.create_autospec(spec=core.BqmlModel) type(mock_bqml_model).session = mock.PropertyMock(return_value=session) + generate_text_tvf = core.BqmlModel.TvfDef( + mock_generate_text, "ml_generate_text_status" + ) # Responses. Retry once, no progress, just stop. - mock_bqml_model.generate_text.side_effect = [ + mock_generate_text.side_effect = [ EqCmpAllDataFrame( { "ml_generate_text_status": ["", "error", "error"], @@ -467,31 +515,32 @@ def test_text_generator_retry_no_progress( ) text_generator_model._bqml_model = mock_bqml_model - # No progress, only conduct retry once - result = text_generator_model.predict(df0, max_retries=3) + with mock.patch.object(core.BqmlModel, "generate_text_tvf", generate_text_tvf): + # No progress, only conduct retry once + result = text_generator_model.predict(df0, max_retries=3) - mock_bqml_model.generate_text.assert_has_calls( - [ - mock.call(df0, options), - mock.call(df1, options), - ] - ) - pd.testing.assert_frame_equal( - result.to_pandas(), - pd.DataFrame( - { - "ml_generate_text_status": ["", "error", "error"], - "prompt": [ - "What is BigQuery?", - "What is BQML?", - "What is BigQuery DataFrame?", - ], - }, - index=[0, 1, 2], - ), - check_dtype=False, - check_index_type=False, - ) + mock_generate_text.assert_has_calls( + [ + mock.call(mock_bqml_model, df0, options), + mock.call(mock_bqml_model, df1, options), + ] + ) + pd.testing.assert_frame_equal( + result.to_pandas(), + pd.DataFrame( + { + "ml_generate_text_status": ["", "error", "error"], + "prompt": [ + "What is BigQuery?", + "What is BQML?", + "What is BigQuery DataFrame?", + ], + }, + index=[0, 1, 2], + ), + check_dtype=False, + check_index_type=False, + ) def test_text_embedding_generator_retry_success(session, bq_connection): @@ -529,11 +578,17 @@ def test_text_embedding_generator_retry_success(session, bq_connection): session=session, ) + mock_generate_embedding = mock.create_autospec( + Callable[[core.BqmlModel, bpd.DataFrame, dict], bpd.DataFrame] + ) mock_bqml_model = mock.create_autospec(spec=core.BqmlModel) type(mock_bqml_model).session = mock.PropertyMock(return_value=session) + generate_embedding_tvf = core.BqmlModel.TvfDef( + mock_generate_embedding, "ml_generate_embedding_status" + ) # Responses. Retry twice then all succeeded. - mock_bqml_model.generate_embedding.side_effect = [ + mock_generate_embedding.side_effect = [ EqCmpAllDataFrame( { "ml_generate_embedding_status": ["", "error", "error"], @@ -568,41 +623,42 @@ def test_text_embedding_generator_retry_success(session, bq_connection): session=session, ), ] - options = { - "flatten_json_output": True, - } + options: dict = {} text_embedding_model = llm.TextEmbeddingGenerator( connection_name=bq_connection, session=session ) text_embedding_model._bqml_model = mock_bqml_model - # 3rd retry isn't triggered - result = text_embedding_model.predict(df0, max_retries=3) - - mock_bqml_model.generate_embedding.assert_has_calls( - [ - mock.call(df0, options), - mock.call(df1, options), - mock.call(df2, options), - ] - ) - pd.testing.assert_frame_equal( - result.to_pandas(), - pd.DataFrame( - { - "ml_generate_embedding_status": ["", "", ""], - "content": [ - "What is BigQuery?", - "What is BigQuery DataFrame?", - "What is BQML?", - ], - }, - index=[0, 2, 1], - ), - check_dtype=False, - check_index_type=False, - ) + with mock.patch.object( + core.BqmlModel, "generate_embedding_tvf", generate_embedding_tvf + ): + # 3rd retry isn't triggered + result = text_embedding_model.predict(df0, max_retries=3) + + mock_generate_embedding.assert_has_calls( + [ + mock.call(mock_bqml_model, df0, options), + mock.call(mock_bqml_model, df1, options), + mock.call(mock_bqml_model, df2, options), + ] + ) + pd.testing.assert_frame_equal( + result.to_pandas(), + pd.DataFrame( + { + "ml_generate_embedding_status": ["", "", ""], + "content": [ + "What is BigQuery?", + "What is BigQuery DataFrame?", + "What is BQML?", + ], + }, + index=[0, 2, 1], + ), + check_dtype=False, + check_index_type=False, + ) def test_text_embedding_generator_retry_no_progress(session, bq_connection): @@ -630,10 +686,17 @@ def test_text_embedding_generator_retry_no_progress(session, bq_connection): session=session, ) + mock_generate_embedding = mock.create_autospec( + Callable[[core.BqmlModel, bpd.DataFrame, dict], bpd.DataFrame] + ) mock_bqml_model = mock.create_autospec(spec=core.BqmlModel) type(mock_bqml_model).session = mock.PropertyMock(return_value=session) + generate_embedding_tvf = core.BqmlModel.TvfDef( + mock_generate_embedding, "ml_generate_embedding_status" + ) + # Responses. Retry once, no progress, just stop. - mock_bqml_model.generate_embedding.side_effect = [ + mock_generate_embedding.side_effect = [ EqCmpAllDataFrame( { "ml_generate_embedding_status": ["", "error", "error"], @@ -658,40 +721,41 @@ def test_text_embedding_generator_retry_no_progress(session, bq_connection): session=session, ), ] - options = { - "flatten_json_output": True, - } + options: dict = {} text_embedding_model = llm.TextEmbeddingGenerator( connection_name=bq_connection, session=session ) text_embedding_model._bqml_model = mock_bqml_model - # No progress, only conduct retry once - result = text_embedding_model.predict(df0, max_retries=3) + with mock.patch.object( + core.BqmlModel, "generate_embedding_tvf", generate_embedding_tvf + ): + # No progress, only conduct retry once + result = text_embedding_model.predict(df0, max_retries=3) - mock_bqml_model.generate_embedding.assert_has_calls( - [ - mock.call(df0, options), - mock.call(df1, options), - ] - ) - pd.testing.assert_frame_equal( - result.to_pandas(), - pd.DataFrame( - { - "ml_generate_embedding_status": ["", "error", "error"], - "content": [ - "What is BigQuery?", - "What is BQML?", - "What is BigQuery DataFrame?", - ], - }, - index=[0, 1, 2], - ), - check_dtype=False, - check_index_type=False, - ) + mock_generate_embedding.assert_has_calls( + [ + mock.call(mock_bqml_model, df0, options), + mock.call(mock_bqml_model, df1, options), + ] + ) + pd.testing.assert_frame_equal( + result.to_pandas(), + pd.DataFrame( + { + "ml_generate_embedding_status": ["", "error", "error"], + "content": [ + "What is BigQuery?", + "What is BQML?", + "What is BigQuery DataFrame?", + ], + }, + index=[0, 1, 2], + ), + check_dtype=False, + check_index_type=False, + ) @pytest.mark.flaky(retries=2) From 53caa8d689e64436f5313095ee27479a06d8e8a8 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 24 Apr 2025 16:21:12 -0700 Subject: [PATCH 14/28] docs: add sample code snippets for `udf` (#1649) * docs: add sample code snippets for `udf` * remove connection cleanup, not neede for udf * use bigframes project for doctest * restore python version agnostic logic for udf --- bigframes/functions/_function_session.py | 15 ++- samples/snippets/conftest.py | 11 +++ samples/snippets/remote_function.py | 4 +- samples/snippets/udf.py | 121 +++++++++++++++++++++++ samples/snippets/udf_test.py | 38 +++++++ 5 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 samples/snippets/udf.py create mode 100644 samples/snippets/udf_test.py diff --git a/bigframes/functions/_function_session.py b/bigframes/functions/_function_session.py index 7fb5cc114b..e18f7084db 100644 --- a/bigframes/functions/_function_session.py +++ b/bigframes/functions/_function_session.py @@ -838,9 +838,18 @@ def wrapper(func): TypeError, f"func must be a callable, got {func}" ) - # Managed function supports version >= 3.11. - signature_kwargs: Mapping[str, Any] = {"eval_str": True} - signature = inspect.signature(func, **signature_kwargs) + if sys.version_info >= (3, 10): + # Add `eval_str = True` so that deferred annotations are turned into their + # corresponding type objects. Need Python 3.10 for eval_str parameter. + # https://docs.python.org/3/library/inspect.html#inspect.signature + signature_kwargs: Mapping[str, Any] = {"eval_str": True} + else: + signature_kwargs = {} # type: ignore + + signature = inspect.signature( + func, + **signature_kwargs, + ) # Try to get input types via type annotations. if input_types is None: diff --git a/samples/snippets/conftest.py b/samples/snippets/conftest.py index 5cba045ce4..e8253bc5a7 100644 --- a/samples/snippets/conftest.py +++ b/samples/snippets/conftest.py @@ -24,6 +24,8 @@ "python-bigquery-dataframes", "samples/snippets" ) +routine_prefixer = test_utils.prefixer.Prefixer("bigframes", "") + @pytest.fixture(scope="session", autouse=True) def cleanup_datasets(bigquery_client: bigquery.Client) -> None: @@ -106,3 +108,12 @@ def random_model_id_eu( full_model_id = f"{project_id}.{dataset_id_eu}.{random_model_id_eu}" yield full_model_id bigquery_client.delete_model(full_model_id, not_found_ok=True) + + +@pytest.fixture +def routine_id() -> Iterator[str]: + """Create a new BQ routine ID each time, so random_routine_id can be used as + target for udf creation. + """ + random_routine_id = routine_prefixer.create_prefix() + yield random_routine_id diff --git a/samples/snippets/remote_function.py b/samples/snippets/remote_function.py index 3a7031ef89..4c5b365007 100644 --- a/samples/snippets/remote_function.py +++ b/samples/snippets/remote_function.py @@ -21,7 +21,7 @@ def run_remote_function_and_read_gbq_function(project_id: str) -> None: # Set BigQuery DataFrames options bpd.options.bigquery.project = your_gcp_project_id - bpd.options.bigquery.location = "us" + bpd.options.bigquery.location = "US" # BigQuery DataFrames gives you the ability to turn your custom scalar # functions into a BigQuery remote function. It requires the GCP project to @@ -56,7 +56,7 @@ def get_bucket(num: float) -> str: boundary = 4000 return "at_or_above_4000" if num >= boundary else "below_4000" - # Then we can apply the remote function on the `Series`` of interest via + # Then we can apply the remote function on the `Series` of interest via # `apply` API and store the result in a new column in the DataFrame. df = df.assign(body_mass_bucket=df["body_mass_g"].apply(get_bucket)) diff --git a/samples/snippets/udf.py b/samples/snippets/udf.py new file mode 100644 index 0000000000..495cd33e84 --- /dev/null +++ b/samples/snippets/udf.py @@ -0,0 +1,121 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def run_udf_and_read_gbq_function( + project_id: str, dataset_id: str, routine_id: str +) -> None: + your_gcp_project_id = project_id + your_bq_dataset_id = dataset_id + your_bq_routine_id = routine_id + + # [START bigquery_dataframes_udf] + import bigframes.pandas as bpd + + # Set BigQuery DataFrames options + bpd.options.bigquery.project = your_gcp_project_id + bpd.options.bigquery.location = "US" + + # BigQuery DataFrames gives you the ability to turn your custom functions + # into a BigQuery Python UDF. One can find more details about the usage and + # the requirements via `help` command. + help(bpd.udf) + + # Read a table and inspect the column of interest. + df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + df["body_mass_g"].peek(10) + + # Define a custom function, and specify the intent to turn it into a + # BigQuery Python UDF. Let's try a `pandas`-like use case in which we want + # to apply a user defined function to every value in a `Series`, more + # specifically bucketize the `body_mass_g` value of the penguins, which is a + # real number, into a category, which is a string. + @bpd.udf( + dataset=your_bq_dataset_id, + name=your_bq_routine_id, + ) + def get_bucket(num: float) -> str: + if not num: + return "NA" + boundary = 4000 + return "at_or_above_4000" if num >= boundary else "below_4000" + + # Then we can apply the udf on the `Series` of interest via + # `apply` API and store the result in a new column in the DataFrame. + df = df.assign(body_mass_bucket=df["body_mass_g"].apply(get_bucket)) + + # This will add a new column `body_mass_bucket` in the DataFrame. You can + # preview the original value and the bucketized value side by side. + df[["body_mass_g", "body_mass_bucket"]].peek(10) + + # The above operation was possible by doing all the computation on the + # cloud through an underlying BigQuery Python UDF that was created to + # support the user's operations in the Python code. + + # The BigQuery Python UDF created to support the BigQuery DataFrames + # udf can be located via a property `bigframes_bigquery_function` + # set in the udf object. + print(f"Created BQ Python UDF: {get_bucket.bigframes_bigquery_function}") + + # If you have already defined a custom function in BigQuery, either via the + # BigQuery Google Cloud Console or with the `udf` decorator, + # or otherwise, you may use it with BigQuery DataFrames with the + # `read_gbq_function` method. More details are available via the `help` + # command. + help(bpd.read_gbq_function) + + existing_get_bucket_bq_udf = get_bucket.bigframes_bigquery_function + + # Here is an example of using `read_gbq_function` to load an existing + # BigQuery Python UDF. + df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins") + get_bucket_function = bpd.read_gbq_function(existing_get_bucket_bq_udf) + + df = df.assign(body_mass_bucket=df["body_mass_g"].apply(get_bucket_function)) + df.peek(10) + + # Let's continue trying other potential use cases of udf. Let's say we + # consider the `species`, `island` and `sex` of the penguins sensitive + # information and want to redact that by replacing with their hash code + # instead. Let's define another scalar custom function and decorate it + # as a udf. The custom function in this example has external package + # dependency, which can be specified via `packages` parameter. + @bpd.udf( + dataset=your_bq_dataset_id, + name=your_bq_routine_id, + packages=["cryptography"], + ) + def get_hash(input: str) -> str: + from cryptography.fernet import Fernet + + # handle missing value + if input is None: + input = "" + + key = Fernet.generate_key() + f = Fernet(key) + return f.encrypt(input.encode()).decode() + + # We can use this udf in another `pandas`-like API `map` that + # can be applied on a DataFrame + df_redacted = df[["species", "island", "sex"]].map(get_hash) + df_redacted.peek(10) + + # [END bigquery_dataframes_udf] + + # Clean up cloud artifacts + session = bpd.get_global_session() + session.bqclient.delete_routine( + f"{your_bq_dataset_id}.{your_bq_routine_id}", not_found_ok=True + ) diff --git a/samples/snippets/udf_test.py b/samples/snippets/udf_test.py new file mode 100644 index 0000000000..a352b4c8ce --- /dev/null +++ b/samples/snippets/udf_test.py @@ -0,0 +1,38 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import bigframes.pandas + +from . import udf + + +def test_udf_and_read_gbq_function( + capsys: pytest.CaptureFixture[str], + dataset_id: str, + routine_id: str, +) -> None: + # We need a fresh session since we're modifying connection options. + bigframes.pandas.close_session() + + # Determine project id, in this case prefer the one set in the environment + # variable GOOGLE_CLOUD_PROJECT (if any) + import os + + your_project_id = os.getenv("GOOGLE_CLOUD_PROJECT", "bigframes-dev") + + udf.run_udf_and_read_gbq_function(your_project_id, dataset_id, routine_id) + out, _ = capsys.readouterr() + assert "Created BQ Python UDF:" in out From 0895ef83ddffb62668ecda7bd08eacb98efea98b Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Fri, 25 Apr 2025 13:20:44 -0700 Subject: [PATCH 15/28] refactor: add empty SQLGlotCompiler classes (#1652) * define configs module for CompileRequest and CompileResult * workaround AttributeError debugging issue * refactor: add empty SQLGlotCompiler --- bigframes/core/compile/api.py | 6 +- bigframes/core/compile/compiler.py | 22 +-- bigframes/core/compile/configs.py | 36 +++++ bigframes/core/compile/sqlglot/__init__.py | 5 + bigframes/core/compile/sqlglot/compiler.py | 141 ++++++++++++++++++ bigframes/core/compile/sqlglot/sql_gen.py | 38 +++++ bigframes/dataframe.py | 22 ++- .../core/compile/sqlglot/compiler_session.py | 76 ++++++++++ tests/unit/core/compile/sqlglot/conftest.py | 29 ++++ .../compile/sqlglot/test_compile_readlocal.py | 24 +++ 10 files changed, 373 insertions(+), 26 deletions(-) create mode 100644 bigframes/core/compile/configs.py create mode 100644 bigframes/core/compile/sqlglot/compiler.py create mode 100644 bigframes/core/compile/sqlglot/sql_gen.py create mode 100644 tests/unit/core/compile/sqlglot/compiler_session.py create mode 100644 tests/unit/core/compile/sqlglot/conftest.py create mode 100644 tests/unit/core/compile/sqlglot/test_compile_readlocal.py diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index bb19f92be9..287549b571 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -18,7 +18,7 @@ import google.cloud.bigquery as bigquery from bigframes.core import rewrite -from bigframes.core.compile import compiler +from bigframes.core.compile import compiler, configs if TYPE_CHECKING: import bigframes.core.nodes @@ -34,7 +34,7 @@ def compile( limit: Optional[int] = None, ) -> str: """Compile node into sql where rows are sorted with ORDER BY.""" - request = compiler.CompileRequest(node, sort_rows=ordered, peek_count=limit) + request = configs.CompileRequest(node, sort_rows=ordered, peek_count=limit) return compiler.compile_sql(request).sql def compile_raw( @@ -44,7 +44,7 @@ def compile_raw( str, Sequence[bigquery.SchemaField], bigframes.core.ordering.RowOrdering ]: """Compile node into sql that exposes all columns, including hidden ordering-only columns.""" - request = compiler.CompileRequest( + request = configs.CompileRequest( node, sort_rows=False, materialize_all_order_keys=True ) result = compiler.compile_sql(request) diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 8ca6cb35a3..a778889925 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -29,6 +29,7 @@ from bigframes.core import expression import bigframes.core.compile.compiled as compiled import bigframes.core.compile.concat as concat_impl +import bigframes.core.compile.configs as configs import bigframes.core.compile.explode import bigframes.core.compile.scalar_op_compiler as compile_scalar import bigframes.core.nodes as nodes @@ -39,22 +40,7 @@ import bigframes.core -@dataclasses.dataclass(frozen=True) -class CompileRequest: - node: nodes.BigFrameNode - sort_rows: bool - materialize_all_order_keys: bool = False - peek_count: typing.Optional[int] = None - - -@dataclasses.dataclass(frozen=True) -class CompileResult: - sql: str - sql_schema: typing.Sequence[google.cloud.bigquery.SchemaField] - row_order: Optional[bf_ordering.RowOrdering] - - -def compile_sql(request: CompileRequest) -> CompileResult: +def compile_sql(request: configs.CompileRequest) -> configs.CompileResult: output_names = tuple((expression.DerefOp(id), id.sql) for id in request.node.ids) result_node = nodes.ResultNode( request.node, @@ -74,7 +60,7 @@ def compile_sql(request: CompileRequest) -> CompileResult: if request.sort_rows: result_node = cast(nodes.ResultNode, rewrites.column_pruning(result_node)) sql = compile_result_node(result_node) - return CompileResult( + return configs.CompileResult( sql, result_node.schema.to_bigquery(), result_node.order_by ) @@ -88,7 +74,7 @@ def compile_sql(request: CompileRequest) -> CompileResult: ordering if ordering.referenced_columns.issubset(result_node.ids) else None ) assert (not request.materialize_all_order_keys) or (output_order is not None) - return CompileResult(sql, result_node.schema.to_bigquery(), output_order) + return configs.CompileResult(sql, result_node.schema.to_bigquery(), output_order) def _replace_unsupported_ops(node: nodes.BigFrameNode): diff --git a/bigframes/core/compile/configs.py b/bigframes/core/compile/configs.py new file mode 100644 index 0000000000..5ffca0cf43 --- /dev/null +++ b/bigframes/core/compile/configs.py @@ -0,0 +1,36 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import dataclasses +import typing + +import google.cloud.bigquery + +from bigframes.core import nodes, ordering + + +@dataclasses.dataclass(frozen=True) +class CompileRequest: + node: nodes.BigFrameNode + sort_rows: bool + materialize_all_order_keys: bool = False + peek_count: typing.Optional[int] = None + + +@dataclasses.dataclass(frozen=True) +class CompileResult: + sql: str + sql_schema: typing.Sequence[google.cloud.bigquery.SchemaField] + row_order: typing.Optional[ordering.RowOrdering] diff --git a/bigframes/core/compile/sqlglot/__init__.py b/bigframes/core/compile/sqlglot/__init__.py index 0a2669d7a2..2f40894975 100644 --- a/bigframes/core/compile/sqlglot/__init__.py +++ b/bigframes/core/compile/sqlglot/__init__.py @@ -11,3 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + +from bigframes.core.compile.sqlglot.compiler import SQLGlotCompiler + +__all__ = ["SQLGlotCompiler"] diff --git a/bigframes/core/compile/sqlglot/compiler.py b/bigframes/core/compile/sqlglot/compiler.py new file mode 100644 index 0000000000..df800c5489 --- /dev/null +++ b/bigframes/core/compile/sqlglot/compiler.py @@ -0,0 +1,141 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import dataclasses +import functools +import typing + +import google.cloud.bigquery as bigquery +import sqlglot.expressions as sge + +from bigframes.core import expression, nodes, rewrite +from bigframes.core.compile import configs +from bigframes.core.compile.sqlglot import sql_gen +import bigframes.core.ordering as bf_ordering + + +@dataclasses.dataclass(frozen=True) +class SQLGlotCompiler: + """Compiles BigFrame nodes into SQL using SQLGlot.""" + + sql_gen = sql_gen.SQLGen() + + def compile( + self, + node: nodes.BigFrameNode, + *, + ordered: bool = True, + limit: typing.Optional[int] = None, + ) -> str: + """Compile node into sql where rows are sorted with ORDER BY.""" + request = configs.CompileRequest(node, sort_rows=ordered, peek_count=limit) + return self._compile_sql(request).sql + + def compile_raw( + self, + node: nodes.BigFrameNode, + ) -> typing.Tuple[ + str, typing.Sequence[bigquery.SchemaField], bf_ordering.RowOrdering + ]: + """Compile node into sql that exposes all columns, including hidden + ordering-only columns.""" + request = configs.CompileRequest( + node, sort_rows=False, materialize_all_order_keys=True + ) + result = self._compile_sql(request) + assert result.row_order is not None + return result.sql, result.sql_schema, result.row_order + + def _compile_sql(self, request: configs.CompileRequest) -> configs.CompileResult: + output_names = tuple( + (expression.DerefOp(id), id.sql) for id in request.node.ids + ) + result_node = nodes.ResultNode( + request.node, + output_cols=output_names, + limit=request.peek_count, + ) + if request.sort_rows: + # Can only pullup slice if we are doing ORDER BY in outermost SELECT + # Need to do this before replacing unsupported ops, as that will rewrite slice ops + result_node = rewrite.pull_up_limits(result_node) + result_node = _replace_unsupported_ops(result_node) + # prune before pulling up order to avoid unnnecessary row_number() ops + result_node = typing.cast(nodes.ResultNode, rewrite.column_pruning(result_node)) + result_node = rewrite.defer_order( + result_node, output_hidden_row_keys=request.materialize_all_order_keys + ) + if request.sort_rows: + result_node = typing.cast( + nodes.ResultNode, rewrite.column_pruning(result_node) + ) + sql = self._compile_result_node(result_node) + return configs.CompileResult( + sql, result_node.schema.to_bigquery(), result_node.order_by + ) + + ordering: typing.Optional[bf_ordering.RowOrdering] = result_node.order_by + result_node = dataclasses.replace(result_node, order_by=None) + result_node = typing.cast(nodes.ResultNode, rewrite.column_pruning(result_node)) + sql = self._compile_result_node(result_node) + # Return the ordering iff no extra columns are needed to define the row order + if ordering is not None: + output_order = ( + ordering + if ordering.referenced_columns.issubset(result_node.ids) + else None + ) + assert (not request.materialize_all_order_keys) or (output_order is not None) + return configs.CompileResult( + sql, result_node.schema.to_bigquery(), output_order + ) + + def _compile_result_node(self, root: nodes.ResultNode) -> str: + sqlglot_expr = compile_node(root.child) + # TODO: add order_by, limit, and selections to sqlglot_expr + return self.sql_gen.sql(sqlglot_expr) + + +def _replace_unsupported_ops(node: nodes.BigFrameNode): + node = nodes.bottom_up(node, rewrite.rewrite_slice) + node = nodes.bottom_up(node, rewrite.rewrite_timedelta_expressions) + node = nodes.bottom_up(node, rewrite.rewrite_range_rolling) + return node + + +@functools.lru_cache(maxsize=5000) +def compile_node(node: nodes.BigFrameNode) -> sge.Expression: + """Compile node into CompileArrayValue. Caches result.""" + return node.reduce_up(lambda node, children: _compile_node(node, *children)) + + +@functools.singledispatch +def _compile_node( + node: nodes.BigFrameNode, *compiled_children: sge.Expression +) -> sge.Expression: + """Defines transformation but isn't cached, always use compile_node instead""" + raise ValueError(f"Can't compile unrecognized node: {node}") + + +@_compile_node.register +def compile_readlocal(node: nodes.ReadLocalNode, *args) -> sge.Expression: + # TODO: add support for reading from local files + return sge.select() + + +@_compile_node.register +def compile_selection(node: nodes.SelectionNode, child: sge.Expression): + # TODO: add support for selection + return child diff --git a/bigframes/core/compile/sqlglot/sql_gen.py b/bigframes/core/compile/sqlglot/sql_gen.py new file mode 100644 index 0000000000..85784e3f5d --- /dev/null +++ b/bigframes/core/compile/sqlglot/sql_gen.py @@ -0,0 +1,38 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses + +import sqlglot.dialects.bigquery +import sqlglot.expressions as sge + + +@dataclasses.dataclass(frozen=True) +class SQLGen: + """Helper class to build SQLGlot Query and generate SQL string.""" + + dialect = sqlglot.dialects.bigquery.BigQuery + """The SQL dialect used for generation.""" + + quoted: bool = True + """Whether to quote identifiers in the generated SQL.""" + + pretty: bool = True + """Whether to pretty-print the generated SQL.""" + + def sql(self, expr: sge.Expression) -> str: + """Generate SQL string from the given expression.""" + return expr.sql(dialect=self.dialect, pretty=self.pretty) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 95ea487786..7ce59760b3 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -419,11 +419,23 @@ def sql(self) -> str: str: string representing the compiled SQL. """ - include_index = self._has_index and ( - self.index.name is not None or len(self.index.names) > 1 - ) - sql, _, _ = self._to_sql_query(include_index=include_index) - return sql + try: + include_index = self._has_index and ( + self.index.name is not None or len(self.index.names) > 1 + ) + sql, _, _ = self._to_sql_query(include_index=include_index) + return sql + except AttributeError as e: + # Workaround for a development-mode debugging issue: + # An `AttributeError` originating *inside* this @property getter (e.g., due to + # a typo or referencing a non-existent attribute) can be mistakenly intercepted + # by the class's __getattr__ method if one is defined. + # We catch the AttributeError and raise SyntaxError instead to make it clear + # the error originates *here* in the property implementation. + # See: https://stackoverflow.com/questions/50542177/correct-handling-of-attributeerror-in-getattr-when-using-property + raise SyntaxError( + "AttributeError encountered. Please check the implementation for incorrect attribute access." + ) from e @property def query_job(self) -> Optional[bigquery.QueryJob]: diff --git a/tests/unit/core/compile/sqlglot/compiler_session.py b/tests/unit/core/compile/sqlglot/compiler_session.py new file mode 100644 index 0000000000..eddae8f891 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/compiler_session.py @@ -0,0 +1,76 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import dataclasses +import typing +import weakref + +import bigframes.core +import bigframes.core.compile.sqlglot as sqlglot +import bigframes.dataframe +import bigframes.session.executor +import bigframes.session.metrics + + +@dataclasses.dataclass +class SQLCompilerExecutor(bigframes.session.executor.Executor): + """Executor for SQL compilation using sqlglot.""" + + compiler = sqlglot.SQLGlotCompiler() + + def to_sql( + self, + array_value: bigframes.core.ArrayValue, + offset_column: typing.Optional[str] = None, + ordered: bool = True, + enable_cache: bool = False, + ) -> str: + if offset_column: + array_value, _ = array_value.promote_offsets() + + # Compared with BigQueryCachingExecutor, SQLCompilerExecutor skips + # caching the subtree. + return self.compiler.compile(array_value.node, ordered=ordered) + + +class SQLCompilerSession(bigframes.session.Session): + """Session for SQL compilation using sqlglot.""" + + def __init__(self): + # TODO: remove unused attributes. + self._location = None # type: ignore + self._bq_kms_key_name = None # type: ignore + self._clients_provider = None # type: ignore + self.ibis_client = None # type: ignore + self._bq_connection = None # type: ignore + self._skip_bq_connection_check = True + self._objects: list[ + weakref.ReferenceType[ + typing.Union[ + bigframes.core.indexes.Index, + bigframes.series.Series, + bigframes.dataframe.DataFrame, + ] + ] + ] = [] + self._strictly_ordered: bool = True + self._allow_ambiguity = False # type: ignore + self._default_index_type = bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64 + self._metrics = bigframes.session.metrics.ExecutionMetrics() + self._remote_function_session = None # type: ignore + self._temp_storage_manager = None # type: ignore + self._loader = None # type: ignore + + self._session_id: str = "sqlglot_unit_tests_session" + self._executor = SQLCompilerExecutor() diff --git a/tests/unit/core/compile/sqlglot/conftest.py b/tests/unit/core/compile/sqlglot/conftest.py new file mode 100644 index 0000000000..c9087abe88 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/conftest.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +import pytest + + +@pytest.fixture(scope="module") +def compiler_session(): + from . import compiler_session + + return compiler_session.SQLCompilerSession() + + +@pytest.fixture(scope="module") +def all_types_df() -> pd.DataFrame: + # TODO: all types pandas dataframes + return pd.DataFrame({}) diff --git a/tests/unit/core/compile/sqlglot/test_compile_readlocal.py b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py new file mode 100644 index 0000000000..2a69bb69b4 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py @@ -0,0 +1,24 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +import bigframes +import bigframes.pandas as bpd + + +def test_compile_local(all_types_df: pd.DataFrame, compiler_session: bigframes.Session): + bf_df = bpd.DataFrame(all_types_df, session=compiler_session) + sql = bf_df.sql + assert sql == "SELECT" From c46ad0647785a9207359eba0fb5b6f7a16610f2a Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 25 Apr 2025 13:26:25 -0700 Subject: [PATCH 16/28] feat: Support write api as loading option (#1617) --- bigframes/core/local_data.py | 78 +++++++++++++++++---- bigframes/core/utils.py | 3 + bigframes/session/__init__.py | 9 ++- bigframes/session/clients.py | 31 ++++++++ bigframes/session/loader.py | 71 +++++++++++++++++-- setup.py | 2 + tests/system/small/test_dataframe.py | 3 +- tests/system/small/test_session.py | 78 ++++++--------------- tests/unit/core/test_bf_utils.py | 2 +- third_party/bigframes_vendored/constants.py | 6 +- 10 files changed, 205 insertions(+), 78 deletions(-) diff --git a/bigframes/core/local_data.py b/bigframes/core/local_data.py index baa29ba72b..d387e0b818 100644 --- a/bigframes/core/local_data.py +++ b/bigframes/core/local_data.py @@ -97,27 +97,46 @@ def from_pyarrow(self, table: pa.Table) -> ManagedArrowTable: mat.validate() return mat - def to_pyarrow_table( + def to_arrow( self, *, offsets_col: Optional[str] = None, geo_format: Literal["wkb", "wkt"] = "wkt", duration_type: Literal["int", "duration"] = "duration", json_type: Literal["string"] = "string", - ) -> pa.Table: - pa_table = self.data - if offsets_col is not None: - pa_table = pa_table.append_column( - offsets_col, pa.array(range(pa_table.num_rows), type=pa.int64()) - ) + ) -> tuple[pa.Schema, Iterable[pa.RecordBatch]]: if geo_format != "wkt": raise NotImplementedError(f"geo format {geo_format} not yet implemented") - if duration_type != "duration": - raise NotImplementedError( - f"duration as {duration_type} not yet implemented" - ) assert json_type == "string" - return pa_table + + batches = self.data.to_batches() + schema = self.data.schema + if duration_type == "int": + schema = _schema_durations_to_ints(schema) + batches = map(functools.partial(_cast_pa_batch, schema=schema), batches) + + if offsets_col is not None: + return schema.append(pa.field(offsets_col, pa.int64())), _append_offsets( + batches, offsets_col + ) + else: + return schema, batches + + def to_pyarrow_table( + self, + *, + offsets_col: Optional[str] = None, + geo_format: Literal["wkb", "wkt"] = "wkt", + duration_type: Literal["int", "duration"] = "duration", + json_type: Literal["string"] = "string", + ) -> pa.Table: + schema, batches = self.to_arrow( + offsets_col=offsets_col, + geo_format=geo_format, + duration_type=duration_type, + json_type=json_type, + ) + return pa.Table.from_batches(batches, schema) def to_parquet( self, @@ -391,6 +410,41 @@ def _physical_type_replacements(dtype: pa.DataType) -> pa.DataType: return dtype +def _append_offsets( + batches: Iterable[pa.RecordBatch], offsets_col_name: str +) -> Iterable[pa.RecordBatch]: + offset = 0 + for batch in batches: + offsets = pa.array(range(offset, offset + batch.num_rows), type=pa.int64()) + batch_w_offsets = pa.record_batch( + [*batch.columns, offsets], + schema=batch.schema.append(pa.field(offsets_col_name, pa.int64())), + ) + offset += batch.num_rows + yield batch_w_offsets + + +@_recursive_map_types +def _durations_to_ints(type: pa.DataType) -> pa.DataType: + if pa.types.is_duration(type): + return pa.int64() + return type + + +def _schema_durations_to_ints(schema: pa.Schema) -> pa.Schema: + return pa.schema( + pa.field(field.name, _durations_to_ints(field.type)) for field in schema + ) + + +# TODO: Use RecordBatch.cast once min pyarrow>=16.0 +def _cast_pa_batch(batch: pa.RecordBatch, schema: pa.Schema) -> pa.RecordBatch: + return pa.record_batch( + [arr.cast(type) for arr, type in zip(batch.columns, schema.types)], + schema=schema, + ) + + def _pairwise(iterable): do_yield = False a = None diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index ee09fc69cb..9731857ea0 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -142,6 +142,9 @@ def label_to_identifier(label: typing.Hashable, strict: bool = False) -> str: identifier = re.sub(r"[^a-zA-Z0-9_]", "", identifier) if not identifier: identifier = "id" + elif identifier[0].isdigit(): + # first character must be letter or underscore + identifier = "_" + identifier return identifier diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index b189c0e194..6379a6f2e8 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -255,6 +255,7 @@ def __init__( session=self, bqclient=self._clients_provider.bqclient, storage_manager=self._temp_storage_manager, + write_client=self._clients_provider.bqstoragewriteclient, default_index_type=self._default_index_type, scan_index_uniqueness=self._strictly_ordered, force_total_order=self._strictly_ordered, @@ -731,7 +732,9 @@ def read_pandas( workload is such that you exhaust the BigQuery load job quota and your data cannot be embedded in SQL due to size or data type limitations. - + * "bigquery_write": + [Preview] Use the BigQuery Storage Write API. This feature + is in public preview. Returns: An equivalent bigframes.pandas.(DataFrame/Series/Index) object @@ -805,6 +808,10 @@ def _read_pandas( return self._loader.read_pandas( pandas_dataframe, method="stream", api_name=api_name ) + elif write_engine == "bigquery_write": + return self._loader.read_pandas( + pandas_dataframe, method="write", api_name=api_name + ) else: raise ValueError(f"Got unexpected write_engine '{write_engine}'") diff --git a/bigframes/session/clients.py b/bigframes/session/clients.py index 86be8bd897..5ef974d565 100644 --- a/bigframes/session/clients.py +++ b/bigframes/session/clients.py @@ -134,6 +134,9 @@ def __init__( self._bqstoragereadclient: Optional[ google.cloud.bigquery_storage_v1.BigQueryReadClient ] = None + self._bqstoragewriteclient: Optional[ + google.cloud.bigquery_storage_v1.BigQueryWriteClient + ] = None self._cloudfunctionsclient: Optional[ google.cloud.functions_v2.FunctionServiceClient ] = None @@ -238,6 +241,34 @@ def bqstoragereadclient(self): return self._bqstoragereadclient + @property + def bqstoragewriteclient(self): + if not self._bqstoragewriteclient: + bqstorage_options = None + if "bqstoragewriteclient" in self._client_endpoints_override: + bqstorage_options = google.api_core.client_options.ClientOptions( + api_endpoint=self._client_endpoints_override["bqstoragewriteclient"] + ) + elif self._use_regional_endpoints: + bqstorage_options = google.api_core.client_options.ClientOptions( + api_endpoint=_BIGQUERYSTORAGE_REGIONAL_ENDPOINT.format( + location=self._location + ) + ) + + bqstorage_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent=self._application_name + ) + self._bqstoragewriteclient = ( + google.cloud.bigquery_storage_v1.BigQueryWriteClient( + client_info=bqstorage_info, + client_options=bqstorage_options, + credentials=self._credentials, + ) + ) + + return self._bqstoragewriteclient + @property def cloudfunctionsclient(self): if not self._cloudfunctionsclient: diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index b053ed3c90..76f12ae438 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -23,6 +23,7 @@ import typing from typing import ( Dict, + Generator, Hashable, IO, Iterable, @@ -36,12 +37,13 @@ import bigframes_vendored.constants as constants import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions +from google.cloud import bigquery_storage_v1 import google.cloud.bigquery as bigquery -import google.cloud.bigquery.table +from google.cloud.bigquery_storage_v1 import types as bq_storage_types import pandas import pyarrow as pa -from bigframes.core import local_data, utils +from bigframes.core import guid, local_data, utils import bigframes.core as core import bigframes.core.blocks as blocks import bigframes.core.schema as schemata @@ -142,6 +144,7 @@ def __init__( self, session: bigframes.session.Session, bqclient: bigquery.Client, + write_client: bigquery_storage_v1.BigQueryWriteClient, storage_manager: bigframes.session.temporary_storage.TemporaryStorageManager, default_index_type: bigframes.enums.DefaultIndexKind, scan_index_uniqueness: bool, @@ -149,6 +152,7 @@ def __init__( metrics: Optional[bigframes.session.metrics.ExecutionMetrics] = None, ): self._bqclient = bqclient + self._write_client = write_client self._storage_manager = storage_manager self._default_index_type = default_index_type self._scan_index_uniqueness = scan_index_uniqueness @@ -165,7 +169,7 @@ def __init__( def read_pandas( self, pandas_dataframe: pandas.DataFrame, - method: Literal["load", "stream"], + method: Literal["load", "stream", "write"], api_name: str, ) -> dataframe.DataFrame: # TODO: Push this into from_pandas, along with index flag @@ -183,6 +187,8 @@ def read_pandas( array_value = self.load_data(managed_data, api_name=api_name) elif method == "stream": array_value = self.stream_data(managed_data) + elif method == "write": + array_value = self.write_data(managed_data) else: raise ValueError(f"Unsupported read method {method}") @@ -198,7 +204,7 @@ def load_data( self, data: local_data.ManagedArrowTable, api_name: Optional[str] = None ) -> core.ArrayValue: """Load managed data into bigquery""" - ordering_col = "bf_load_job_offsets" + ordering_col = guid.generate_guid("load_offsets_") # JSON support incomplete for item in data.schema.items: @@ -244,7 +250,7 @@ def load_data( def stream_data(self, data: local_data.ManagedArrowTable) -> core.ArrayValue: """Load managed data into bigquery""" - ordering_col = "bf_stream_job_offsets" + ordering_col = guid.generate_guid("stream_offsets_") schema_w_offsets = data.schema.append( schemata.SchemaItem(ordering_col, bigframes.dtypes.INT_DTYPE) ) @@ -277,6 +283,61 @@ def stream_data(self, data: local_data.ManagedArrowTable) -> core.ArrayValue: n_rows=data.data.num_rows, ).drop_columns([ordering_col]) + def write_data(self, data: local_data.ManagedArrowTable) -> core.ArrayValue: + """Load managed data into bigquery""" + ordering_col = guid.generate_guid("stream_offsets_") + schema_w_offsets = data.schema.append( + schemata.SchemaItem(ordering_col, bigframes.dtypes.INT_DTYPE) + ) + bq_schema = schema_w_offsets.to_bigquery(_STREAM_JOB_TYPE_OVERRIDES) + bq_table_ref = self._storage_manager.create_temp_table( + bq_schema, [ordering_col] + ) + + requested_stream = bq_storage_types.stream.WriteStream() + requested_stream.type_ = bq_storage_types.stream.WriteStream.Type.COMMITTED # type: ignore + + stream_request = bq_storage_types.CreateWriteStreamRequest( + parent=bq_table_ref.to_bqstorage(), write_stream=requested_stream + ) + stream = self._write_client.create_write_stream(request=stream_request) + + def request_gen() -> Generator[bq_storage_types.AppendRowsRequest, None, None]: + schema, batches = data.to_arrow( + offsets_col=ordering_col, duration_type="int" + ) + offset = 0 + for batch in batches: + request = bq_storage_types.AppendRowsRequest( + write_stream=stream.name, offset=offset + ) + request.arrow_rows.writer_schema.serialized_schema = ( + schema.serialize().to_pybytes() + ) + request.arrow_rows.rows.serialized_record_batch = ( + batch.serialize().to_pybytes() + ) + offset += batch.num_rows + yield request + + for response in self._write_client.append_rows(requests=request_gen()): + if response.row_errors: + raise ValueError( + f"Problem loading at least one row from DataFrame: {response.row_errors}. {constants.FEEDBACK_LINK}" + ) + # This step isn't strictly necessary in COMMITTED mode, but avoids max active stream limits + response = self._write_client.finalize_write_stream(name=stream.name) + assert response.row_count == data.data.num_rows + + destination_table = self._bqclient.get_table(bq_table_ref) + return core.ArrayValue.from_table( + table=destination_table, + schema=schema_w_offsets, + session=self._session, + offsets_col=ordering_col, + n_rows=data.data.num_rows, + ).drop_columns([ordering_col]) + def _start_generic_job(self, job: formatting_helpers.GenericJob): if bigframes.options.display.progress_bar is not None: formatting_helpers.wait_for_job( diff --git a/setup.py b/setup.py index edc77e11b6..532ec07511 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,8 @@ "google-cloud-bigtable >=2.24.0", "google-cloud-pubsub >=2.21.4", "google-cloud-bigquery[bqstorage,pandas] >=3.31.0", + # 2.30 needed for arrow support. + "google-cloud-bigquery-storage >= 2.30.0, < 3.0.0", "google-cloud-functions >=1.12.0", "google-cloud-bigquery-connection >=1.12.0", "google-cloud-iam >=2.12.1", diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 83901562d3..452fa841e4 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -83,6 +83,7 @@ def test_df_construct_pandas_default(scalars_dfs): ("bigquery_inline"), ("bigquery_load"), ("bigquery_streaming"), + ("bigquery_write"), ], ) def test_read_pandas_all_nice_types( @@ -1772,7 +1773,7 @@ def test_len(scalars_dfs): ) @pytest.mark.parametrize( "write_engine", - ["bigquery_load", "bigquery_streaming"], + ["bigquery_load", "bigquery_streaming", "bigquery_write"], ) def test_df_len_local(session, n_rows, write_engine): assert ( diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 668c764645..1b886fbc5a 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -38,6 +38,17 @@ import bigframes.ml.linear_model from tests.system import utils +all_write_engines = pytest.mark.parametrize( + "write_engine", + [ + "default", + "bigquery_inline", + "bigquery_load", + "bigquery_streaming", + "bigquery_write", + ], +) + @pytest.fixture(scope="module") def df_and_local_csv(scalars_df_index): @@ -865,10 +876,7 @@ def test_read_pandas_tokyo( assert len(expected) == result.total_rows -@pytest.mark.parametrize( - "write_engine", - ["default", "bigquery_inline", "bigquery_load", "bigquery_streaming"], -) +@all_write_engines def test_read_pandas_timedelta_dataframes(session, write_engine): pytest.importorskip( "pandas", @@ -886,10 +894,7 @@ def test_read_pandas_timedelta_dataframes(session, write_engine): pd.testing.assert_frame_equal(actual_result, expected_result) -@pytest.mark.parametrize( - "write_engine", - ["default", "bigquery_inline", "bigquery_load", "bigquery_streaming"], -) +@all_write_engines def test_read_pandas_timedelta_series(session, write_engine): expected_series = pd.Series(pd.to_timedelta([1, 2, 3], unit="d")) @@ -904,10 +909,7 @@ def test_read_pandas_timedelta_series(session, write_engine): ) -@pytest.mark.parametrize( - "write_engine", - ["default", "bigquery_inline", "bigquery_load", "bigquery_streaming"], -) +@all_write_engines def test_read_pandas_timedelta_index(session, write_engine): expected_index = pd.to_timedelta( [1, 2, 3], unit="d" @@ -922,15 +924,7 @@ def test_read_pandas_timedelta_index(session, write_engine): pd.testing.assert_index_equal(actual_result, expected_index) -@pytest.mark.parametrize( - ("write_engine"), - [ - pytest.param("default"), - pytest.param("bigquery_load"), - pytest.param("bigquery_streaming"), - pytest.param("bigquery_inline"), - ], -) +@all_write_engines def test_read_pandas_json_dataframes(session, write_engine): json_data = [ "1", @@ -949,15 +943,7 @@ def test_read_pandas_json_dataframes(session, write_engine): pd.testing.assert_frame_equal(actual_result, expected_df, check_index_type=False) -@pytest.mark.parametrize( - ("write_engine"), - [ - pytest.param("default"), - pytest.param("bigquery_load"), - pytest.param("bigquery_streaming"), - pytest.param("bigquery_inline"), - ], -) +@all_write_engines def test_read_pandas_json_series(session, write_engine): json_data = [ "1", @@ -975,15 +961,7 @@ def test_read_pandas_json_series(session, write_engine): ) -@pytest.mark.parametrize( - ("write_engine"), - [ - pytest.param("default"), - pytest.param("bigquery_inline"), - pytest.param("bigquery_load"), - pytest.param("bigquery_streaming"), - ], -) +@all_write_engines def test_read_pandas_json_series_w_invalid_json(session, write_engine): json_data = [ "False", # Should be "false" @@ -994,15 +972,7 @@ def test_read_pandas_json_series_w_invalid_json(session, write_engine): session.read_pandas(pd_s, write_engine=write_engine) -@pytest.mark.parametrize( - ("write_engine"), - [ - pytest.param("default"), - pytest.param("bigquery_load"), - pytest.param("bigquery_streaming"), - pytest.param("bigquery_inline", marks=pytest.mark.xfail(raises=ValueError)), - ], -) +@all_write_engines def test_read_pandas_json_index(session, write_engine): json_data = [ "1", @@ -1049,6 +1019,7 @@ def test_read_pandas_w_nested_json_fails(session, write_engine): pytest.param("default"), pytest.param("bigquery_inline"), pytest.param("bigquery_streaming"), + pytest.param("bigquery_write"), ], ) def test_read_pandas_w_nested_json(session, write_engine): @@ -1134,6 +1105,7 @@ def test_read_pandas_w_nested_json_index_fails(session, write_engine): pytest.param("default"), pytest.param("bigquery_inline"), pytest.param("bigquery_streaming"), + pytest.param("bigquery_write"), ], ) def test_read_pandas_w_nested_json_index(session, write_engine): @@ -1156,15 +1128,7 @@ def test_read_pandas_w_nested_json_index(session, write_engine): pd.testing.assert_index_equal(bq_idx, pd_idx) -@pytest.mark.parametrize( - ("write_engine",), - ( - ("default",), - ("bigquery_inline",), - ("bigquery_load",), - ("bigquery_streaming",), - ), -) +@all_write_engines def test_read_csv_for_gcs_file_w_write_engine(session, df_and_gcs_csv, write_engine): scalars_df, path = df_and_gcs_csv diff --git a/tests/unit/core/test_bf_utils.py b/tests/unit/core/test_bf_utils.py index cb3b03d988..9b4c4f8742 100644 --- a/tests/unit/core/test_bf_utils.py +++ b/tests/unit/core/test_bf_utils.py @@ -46,7 +46,7 @@ def test_get_standardized_ids_indexes(): assert col_ids == ["duplicate_2"] assert idx_ids == [ "string", - "0", + "_0", utils.UNNAMED_INDEX_ID, "duplicate", "duplicate_1", diff --git a/third_party/bigframes_vendored/constants.py b/third_party/bigframes_vendored/constants.py index d11d8ba2cb..af87694cd5 100644 --- a/third_party/bigframes_vendored/constants.py +++ b/third_party/bigframes_vendored/constants.py @@ -47,6 +47,10 @@ ) WriteEngineType = Literal[ - "default", "bigquery_inline", "bigquery_load", "bigquery_streaming" + "default", + "bigquery_inline", + "bigquery_load", + "bigquery_streaming", + "bigquery_write", ] VALID_WRITE_ENGINES = typing.get_args(WriteEngineType) From 30a62372b2fd72deaf7dbc1dbce8b48f03b3041c Mon Sep 17 00:00:00 2001 From: Garrett Wu <6505921+GarrettWu@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:17:43 -0700 Subject: [PATCH 17/28] chore: add blob display size options (#1657) --- bigframes/_config/experiment_options.py | 31 +++++++++++++++++++++++++ bigframes/dataframe.py | 20 +++++++++++++--- bigframes/operations/blob.py | 7 ++++-- samples/snippets/multimodal_test.py | 8 +++++-- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/bigframes/_config/experiment_options.py b/bigframes/_config/experiment_options.py index bb3966839c..cfbcc04cfd 100644 --- a/bigframes/_config/experiment_options.py +++ b/bigframes/_config/experiment_options.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional import warnings import bigframes.exceptions as bfe @@ -26,6 +27,9 @@ def __init__(self): self._semantic_operators: bool = False self._ai_operators: bool = False self._blob: bool = False + self._blob_display: bool = True + self._blob_display_width: Optional[int] = None + self._blob_display_height: Optional[int] = None @property def semantic_operators(self) -> bool: @@ -67,3 +71,30 @@ def blob(self, value: bool): ) warnings.warn(msg, category=bfe.PreviewWarning) self._blob = value + + @property + def blob_display(self) -> bool: + """Whether to display the blob content in notebook DataFrame preview. Default True.""" + return self._blob_display + + @blob_display.setter + def blob_display(self, value: bool): + self._blob_display = value + + @property + def blob_display_width(self) -> Optional[int]: + """Width in pixels that the blob constrained to.""" + return self._blob_display_width + + @blob_display_width.setter + def blob_display_width(self, value: Optional[int]): + self._blob_display_width = value + + @property + def blob_display_height(self) -> Optional[int]: + """Height in pixels that the blob constrained to.""" + return self._blob_display_height + + @blob_display_height.setter + def blob_display_height(self, value: Optional[int]): + self._blob_display_height = value diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 7ce59760b3..101e7a1c5e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -765,7 +765,10 @@ def _repr_html_(self) -> str: return formatter.repr_query_job(self._compute_dry_run()) df = self.copy() - if bigframes.options.experiments.blob: + if ( + bigframes.options.experiments.blob + and bigframes.options.experiments.blob_display + ): blob_cols = [ col for col in df.columns @@ -788,7 +791,10 @@ def _repr_html_(self) -> str: with display_options.pandas_repr(opts): # Allows to preview images in the DataFrame. The implementation changes the string repr as well, that it doesn't truncate strings or escape html charaters such as "<" and ">". We may need to implement a full-fledged repr module to better support types not in pandas. - if bigframes.options.experiments.blob: + if ( + bigframes.options.experiments.blob + and bigframes.options.experiments.blob_display + ): def obj_ref_rt_to_html(obj_ref_rt) -> str: obj_ref_rt_json = json.loads(obj_ref_rt) @@ -799,8 +805,16 @@ def obj_ref_rt_to_html(obj_ref_rt) -> str: str, gcs_metadata.get("content_type", "") ) if content_type.startswith("image"): + size_str = "" + if bigframes.options.experiments.blob_display_width: + size_str = f' width="{bigframes.options.experiments.blob_display_width}"' + if bigframes.options.experiments.blob_display_height: + size_str = ( + size_str + + f' height="{bigframes.options.experiments.blob_display_height}"' + ) url = obj_ref_rt_json["access_urls"]["read_url"] - return f'' + return f'' return f'uri: {obj_ref_rt_json["objectref"]["uri"]}, authorizer: {obj_ref_rt_json["objectref"]["authorizer"]}' diff --git a/bigframes/operations/blob.py b/bigframes/operations/blob.py index f0e25852cb..2c6e5fca7f 100644 --- a/bigframes/operations/blob.py +++ b/bigframes/operations/blob.py @@ -241,9 +241,12 @@ def display( Args: n (int, default 3): number of sample blob objects to display. content_type (str, default ""): content type of the blob. If unset, use the blob metadata of the storage. Possible values are "image", "audio" and "video". - width (int or None, default None): width in pixels that the image/video are constrained to. If unset, use the image/video's original size or ratio. No-op for other content types. - height (int or None, default None): height in pixels that the image/video are constrained to. If unset, use the image/video's original size or ratio. No-op for other content types. + width (int or None, default None): width in pixels that the image/video are constrained to. If unset, use the global setting in bigframes.options.experiments.blob_display_width, otherwise image/video's original size or ratio is used. No-op for other content types. + height (int or None, default None): height in pixels that the image/video are constrained to. If unset, use the global setting in bigframes.options.experiments.blob_display_height, otherwise image/video's original size or ratio is used. No-op for other content types. """ + width = width or bigframes.options.experiments.blob_display_width + height = height or bigframes.options.experiments.blob_display_height + # col name doesn't matter here. Rename to avoid column name conflicts df = bigframes.series.Series(self._block).rename("blob_col").to_frame() diff --git a/samples/snippets/multimodal_test.py b/samples/snippets/multimodal_test.py index 85e118d671..27a7998ff9 100644 --- a/samples/snippets/multimodal_test.py +++ b/samples/snippets/multimodal_test.py @@ -21,6 +21,8 @@ def test_multimodal_dataframe(gcs_dst_bucket: str) -> None: # Flag to enable the feature bigframes.options.experiments.blob = True + # Flags to control preview image/video preview size + bigframes.options.experiments.blob_display_width = 300 import bigframes.pandas as bpd @@ -47,10 +49,12 @@ def test_multimodal_dataframe(gcs_dst_bucket: str) -> None: df_image["size"] = df_image["image"].blob.size() df_image["updated"] = df_image["image"].blob.updated() df_image + # [END bigquery_dataframes_multimodal_dataframe_merge] + # [START bigquery_dataframes_multimodal_dataframe_filter] # Filter images and display, you can also display audio and video types. Use width/height parameters to constrain window sizes. - df_image[df_image["author"] == "alice"]["image"].blob.display(width=400) - # [END bigquery_dataframes_multimodal_dataframe_merge] + df_image[df_image["author"] == "alice"]["image"].blob.display() + # [END bigquery_dataframes_multimodal_dataframe_filter] # [START bigquery_dataframes_multimodal_dataframe_image_transform] df_image["blurred"] = df_image["image"].blob.image_blur( From ebb7a5e2b24fa57d6fe6a76d9b857ad44c67d194 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 28 Apr 2025 17:50:32 -0400 Subject: [PATCH 18/28] fix: resolve issue where pre-release versions of google-auth are installed (#1491) Co-authored-by: Huan Chen <142538604+Genesis929@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 532ec07511..d0ee4da405 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "fsspec >=2023.3.0", "gcsfs >=2023.3.0", "geopandas >=0.12.2", - "google-auth >=2.15.0,<3.0dev", + "google-auth >=2.15.0,<3.0", "google-cloud-bigtable >=2.24.0", "google-cloud-pubsub >=2.21.4", "google-cloud-bigquery[bqstorage,pandas] >=3.31.0", From f442e7a07ff273ba3af74eeabafb62110b78f692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Swe=C3=B1a=20=28Swast=29?= Date: Mon, 28 Apr 2025 17:54:58 -0500 Subject: [PATCH 19/28] docs: fix `bq_dataframes_template` notebook to work if partial ordering mode is enabled (#1665) * docs: fix `bq_dataframes_template` notebook to work if partial ordering mode is enabled * use reset_index in plot, show output for generative ai --- .../bq_dataframes_template.ipynb | 549 ++++++++++-------- 1 file changed, 298 insertions(+), 251 deletions(-) diff --git a/notebooks/getting_started/bq_dataframes_template.ipynb b/notebooks/getting_started/bq_dataframes_template.ipynb index c79efb63e5..12847483ac 100644 --- a/notebooks/getting_started/bq_dataframes_template.ipynb +++ b/notebooks/getting_started/bq_dataframes_template.ipynb @@ -114,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -137,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": { "id": "oM1iC_MfAts1" }, @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": { "id": "PyQmSRbKA8r-" }, @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "id": "NPPMuw2PXGeo" }, @@ -199,7 +199,7 @@ "# Note: BigQuery DataFrames objects are by default fully ordered like Pandas.\n", "# If ordering is not important for you, you can uncomment the following\n", "# expression to run BigQuery DataFrames in partial ordering mode.\n", - "#bpd.options.bigquery.ordering_mode = \"partial\"\n", + "# bpd.options.bigquery.ordering_mode = \"partial\"\n", "\n", "# Note: By default BigQuery DataFrames emits out BigQuery job metadata via a\n", "# progress bar. But in this notebook let's disable the progress bar to keep the\n", @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": { "id": "Vyex9BQI-BNa" }, @@ -271,7 +271,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -306,53 +306,53 @@ " \n", " \n", " \n", - " 198\n", - " Gentoo penguin (Pygoscelis papua)\n", - " Biscoe\n", - " 43.3\n", - " 13.4\n", - " 209.0\n", - " 4400.0\n", + " 0\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " Dream\n", + " 36.6\n", + " 18.4\n", + " 184.0\n", + " 3475.0\n", " FEMALE\n", " \n", " \n", - " 235\n", + " 1\n", " Adelie Penguin (Pygoscelis adeliae)\n", - " Torgersen\n", - " 35.1\n", - " 19.4\n", - " 193.0\n", - " 4200.0\n", + " Dream\n", + " 39.8\n", + " 19.1\n", + " 184.0\n", + " 4650.0\n", " MALE\n", " \n", " \n", - " 317\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", + " 2\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 45.4\n", - " 18.7\n", - " 188.0\n", - " 3525.0\n", - " FEMALE\n", + " 40.9\n", + " 18.9\n", + " 184.0\n", + " 3900.0\n", + " MALE\n", " \n", " \n", - " 117\n", + " 3\n", " Chinstrap penguin (Pygoscelis antarctica)\n", " Dream\n", - " 48.5\n", - " 17.5\n", - " 191.0\n", - " 3400.0\n", - " MALE\n", + " 46.5\n", + " 17.9\n", + " 192.0\n", + " 3500.0\n", + " FEMALE\n", " \n", " \n", - " 159\n", - " Chinstrap penguin (Pygoscelis antarctica)\n", + " 4\n", + " Adelie Penguin (Pygoscelis adeliae)\n", " Dream\n", - " 45.6\n", - " 19.4\n", - " 194.0\n", - " 3525.0\n", + " 37.3\n", + " 16.8\n", + " 192.0\n", + " 3000.0\n", " FEMALE\n", " \n", " \n", @@ -360,22 +360,22 @@ "" ], "text/plain": [ - " species island culmen_length_mm \\\n", - "198 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", - "235 Adelie Penguin (Pygoscelis adeliae) Torgersen 35.1 \n", - "317 Chinstrap penguin (Pygoscelis antarctica) Dream 45.4 \n", - "117 Chinstrap penguin (Pygoscelis antarctica) Dream 48.5 \n", - "159 Chinstrap penguin (Pygoscelis antarctica) Dream 45.6 \n", + " species island culmen_length_mm \\\n", + "0 Adelie Penguin (Pygoscelis adeliae) Dream 36.6 \n", + "1 Adelie Penguin (Pygoscelis adeliae) Dream 39.8 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Dream 40.9 \n", + "3 Chinstrap penguin (Pygoscelis antarctica) Dream 46.5 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Dream 37.3 \n", "\n", - " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "198 13.4 209.0 4400.0 FEMALE \n", - "235 19.4 193.0 4200.0 MALE \n", - "317 18.7 188.0 3525.0 FEMALE \n", - "117 17.5 191.0 3400.0 MALE \n", - "159 19.4 194.0 3525.0 FEMALE " + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 18.4 184.0 3475.0 FEMALE \n", + "1 19.1 184.0 4650.0 MALE \n", + "2 18.9 184.0 3900.0 MALE \n", + "3 17.9 192.0 3500.0 FEMALE \n", + "4 16.8 192.0 3000.0 FEMALE " ] }, - "execution_count": 5, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -424,7 +424,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": { "id": "YKwCW7Nsavap" }, @@ -433,7 +433,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "average_body_mass: 4201.754385964906\n" + "average_body_mass: 4201.754385964914\n" ] } ], @@ -453,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": { "id": "4PyKMR61-Mjy" }, @@ -514,7 +514,7 @@ "[3 rows x 1 columns]" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -543,7 +543,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -559,21 +559,21 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "133 {'culmen_length_mm': None, 'culmen_depth_mm': ...\n", - "279 {'culmen_length_mm': 37.9, 'culmen_depth_mm': ...\n", - "34 {'culmen_length_mm': 37.8, 'culmen_depth_mm': ...\n", - "208 {'culmen_length_mm': 40.5, 'culmen_depth_mm': ...\n", - "96 {'culmen_length_mm': 37.7, 'culmen_depth_mm': ...\n", + "0 {'culmen_length_mm': 36.6, 'culmen_depth_mm': ...\n", + "1 {'culmen_length_mm': 39.8, 'culmen_depth_mm': ...\n", + "2 {'culmen_length_mm': 40.9, 'culmen_depth_mm': ...\n", + "3 {'culmen_length_mm': 46.5, 'culmen_depth_mm': ...\n", + "4 {'culmen_length_mm': 37.3, 'culmen_depth_mm': ...\n", "dtype: struct[pyarrow]" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -594,21 +594,21 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "133 \n", - "279 18.6\n", - "34 18.3\n", - "96 18.7\n", - "208 18.9\n", + "0 18.4\n", + "1 19.1\n", + "2 18.9\n", + "3 17.9\n", + "4 16.8\n", "dtype: Float64" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -639,22 +639,32 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:263: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n", + " warnings.warn(msg, category=bfe.AmbiguousWindowWarning)\n", + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:239: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n", + " warnings.warn(msg, bfe.AmbiguousWindowWarning)\n" + ] + }, { "data": { "text/plain": [ "" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAAGzCAYAAAAxPS2EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOx9d7wVxfn+s2dPuf1eelGQXhWlWIgNkYj5qrFjxwKWCBpsEIyF2MUWorGhghJLjL9ITFSwYgEVJBZsIB1pV9qFW885u/v74+zMzszO7O45t3HNPp8PH+45uzs7u2d35p3nfd731SzLshAiRIgQIUKECNGCEGnuDoQIESJEiBAhQmSL0IAJESJEiBAhQrQ4hAZMiBAhQoQIEaLFITRgQoQIESJEiBAtDqEBEyJEiBAhQoRocQgNmBAhQoQIESJEi0NowIQIESJEiBAhWhxCAyZEiBAhQoQI0eIQGjAhQoQIESJEiBaH0IAJESJEs0LTNEybNq1B2qqsrMT48ePRsWNHaJqGSZMmNUi7IUKE2PsQGjAhQuzFmD17NjRNQ15eHjZu3OjaPmLECOy///7N0LO9E3fddRdmz56N3/3ud5gzZw4uuOCCRjnPo48+itmzZzdK2yFChAiGaHN3IESIEP6oq6vDPffcg4cffri5u9LgqKmpQTTaMEPRe++9h8MOOwy33nprg7SnwqOPPoq2bdvioosuatTzhAgRQo2QgQkRogXgoIMOwsyZM7Fp06bm7kqDwDRN1NbWAgDy8vIazIApLy9HWVlZg7TV1LAsCzU1Nc3djRAhWgxCAyZEiBaAG2+8EYZh4J577vHcb+3atdA0TereELUm06ZNg6ZpWLFiBc4//3yUlpaiXbt2uPnmm2FZFjZs2ICTTz4ZJSUl6NixIx544AFXm3V1dbj11lvRq1cvJBIJdOnSBZMnT0ZdXZ3r3BMnTsTzzz+PgQMHIpFIYN68edJ+AcDGjRsxbtw4dO7cGYlEAt27d8fvfvc7JJNJ6XUvWLAAmqZhzZo1eP3116FpGjRNw9q1a7Pq56xZszBy5Ei0b98eiUQCAwYMwGOPPcbt061bN3z77bf44IMP6HlGjBjB3VMRxBVI+kPaOfHEEzF//nwMGzYM+fn5eOKJJwAAu3btwqRJk9ClSxckEgn06tUL9957L0zT5Np96aWXMHToUBQXF6OkpAQHHHAAZsyYIb1HIUL80hC6kEKEaAHo3r07xo4di5kzZ+IPf/gDOnfu3GBtn3XWWejfvz/uuecevP7667jjjjvQunVrPPHEExg5ciTuvfdePP/887j++utx8MEH46ijjgKQYVF++9vf4uOPP8Zll12G/v37Y9myZXjooYewYsUKzJ07lzvPe++9h5dffhkTJ05E27Zt0a1bN2l/Nm3ahEMOOQS7du3CZZddhn79+mHjxo145ZVXUF1djXg87jqmf//+mDNnDq655hrsu+++uO666wAA7dq1y6qfjz32GAYOHIjf/va3iEaj+Pe//40rr7wSpmliwoQJAIA///nPuOqqq1BUVIQ//vGPAIAOHTrkdO+XL1+Oc845B5dffjkuvfRS9O3bF9XV1Tj66KOxceNGXH755ejatSsWLVqEqVOnYvPmzfjzn/8MAHj77bdxzjnn4Nhjj8W9994LAPj++++xcOFC/P73v8+pPyFCtChYIUKE2Gsxa9YsC4C1ZMkSa9WqVVY0GrWuvvpquv3oo4+2Bg4cSD+vWbPGAmDNmjXL1RYA69Zbb6Wfb731VguAddlll9Hv0um0te+++1qapln33HMP/X7nzp1Wfn6+deGFF9Lv5syZY0UiEeujjz7izvP4449bAKyFCxdy545EIta3337r26+xY8dakUjEWrJkiWtf0zRd37HYb7/9rBNOOIH7Lpt+VldXu9ocPXq01aNHD+67gQMHWkcffbRrX3JPRZDfcc2aNVxfAVjz5s3j9r399tutwsJCa8WKFdz3f/jDHyxd163169dblmVZv//9762SkhIrnU67zhcixP8CQhdSiBAtBD169MAFF1yAJ598Eps3b26wdsePH0//1nUdw4YNg2VZGDduHP2+rKwMffv2xerVq+l3//jHP9C/f3/069cP27Zto/9GjhwJAHj//fe58xx99NEYMGCAZ19M08TcuXNx0kknYdiwYa7tMveMH7LpZ35+Pv27oqIC27Ztw9FHH43Vq1ejoqIi63P7oXv37hg9erSrv0ceeSRatWrF9XfUqFEwDAMffvghgMxvUlVVhbfffrvB+xUiREtA6EIKEaIF4aabbsKcOXNwzz33NJjWoWvXrtzn0tJS5OXloW3btq7vt2/fTj//+OOP+P7779GuXTtpu+Xl5dzn7t27+/bl559/xu7duxs0NDybfi5cuBC3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n332QWFhIQ499FAsWLCAbieusfnz56N///4oKirC8ccfj82bN9N90uk0rr76apSVlaFNmzaYMmUKLrzwQpxyyikAgIsuuggffPABZsyYAU3TXPdt6dKlGDZsGAoKCvCrX/0Ky5cvD9T3XO+xpml44okncOKJJ6KgoAD9+/fHJ598gpUrV2LEiBEoLCzEr371K6xatSqne6rC9lmzUH7/A6iYO7dB2w0RIkSIhsb/ZB4YETUpAwNumd8s5/7uttEoiAf/GaZOnYqZM2fioYcewhFHHIHNmzfjhx9+yPn8q1atwptvvol58+Zh1apVOOOMM7B69Wr06dMHH3zwARYtWoRLLrkEo0aNwqGHHgoAOPPMM5Gfn48333wTpaWleOKJJ3DsscdixYoVaN26NW137ty5+M9//oOdO3dizJgxuOeee1wTdRBMnDgR3333HV566SV07twZr776Ko4//ngsW7YMvXv3BgBUV1fj/vvvx5w5cxCJRHD++efj+uuvx/PPPw8AuPfee/H8889j1qxZ6N+/P2bMmIG5c+fimGOOAQDMmDEDK1aswP7774/bbrsNANCuXTtqxPzxj3/EAw88gHbt2uGKK67AJZdcgoULFzbaPQaA22+/HQ8++CAefPBBTJkyBeeeey569OiBqVOnomvXrrjkkkswceJEvPnmm1nfUxXMyioAgFFZ2WBthggRIkRjIDRgWhD27NmDGTNm4JFHHsGFF14IAOjZsyeOOOIIjpHIBqZp4plnnkFxcTEGDBiAY445BsuXL8cbb7yBSCSCvn374t5778X777+PQw89FB9//DEWL16M8vJyJBIJAMD999+PuXPn4pVXXsFll11G2509ezaKi4sBABdccAHefffdrA2Y9evXY9asWVi/fj06d+4MALj++usxb948zJo1C3fddRcAIJVK4fHHH0fPnj0BZIweYogAwMMPP4ypU6fi1FNPBQA88sgjeOONN+j20tJSxONxFBQUoGPHjq5+3HnnnTj66KMBAH/4wx9wwgknoLa2NlCCuGzvMcHFF1+MMWPGAACmTJmC4cOH4+abb8bo0aMBAL///e9x8cUXB7+ZQUDqgpnZMYMhQoQI0dQIDRgA+TEd3902utnOHRTff/896urqcOyxxzbY+bt160aNDADo0KEDdF3nkqB16NAB5eXlAICvvvoKlZWVaNOmDddO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", + "image/png": "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adSoEU8//TRr1qxRXzt06FDy8/OpV68eY8eOZenSpRgMhiu28eTJk+j1elq3bq0+1qBBA7y8vCr9Pv9rJJMihBDVQOPkRKP9+6rt2DWJvb09I0aMYMGCBdxzzz0sXryY2bNnX9U+rO9yrNFoytz1WKPR2NS3DBw4kDp16jBv3jyCg4MxmUw0bdqUoqIiAFq3bk1MTAwrV65k3bp1DBs2jL59+/LLL78QEhLCyZMnWbduHWvXruWpp57i/fffZ/Pmzdf1bsv/RRKkCCFENdBoNFc15FKddu3aZfPzzp07CQ8PJzIyEoPBwK5du9ThntTUVE6ePElkZCQAERER5b6+tDFjxtC0aVM+++wzDAYD99xzz3V6N5faOG/ePLp16wbAtm3bymzn7u7O/fffz/333899991H//79SUtLw9vbGycnJwYOHMjAgQMZP348jRs35siRIzZZktIaNWqEwWDgwIEDtGljHuqLiooiPT39+rzRW4AEKUIIIS4rNjaWyZMn8/jjj7N//34++eQTPvzwQ8LDwxk0aBBjx45l7ty5uLm58eKLL1KrVi0GDRoEwNNPP02XLl344IMPGDRoEKtXr7apR7GIiIigY8eOTJkyhcceewyn65jt8fLywsfHhy+//JKgoCBiY2N58cUXbbb56KOPCAoKolWrVmi1Wn7++WcCAwPx9PRk4cKFGI1GOnTogLOzM9999x1OTk42dSvlady4MX379mXcuHF8/vnn2NnZ8eyzz+J0lYXM/yVSkyKEEOKyRowYQX5+Pu3bt2f8+PE888wzjBs3DjDPwmnTpg133XUXnTp1QlEUVqxYoQ57dOzYkXnz5jF79mxatGjBmjVrePXVV8s9zujRoykqKuKxxx67ru9Hq9WyZMkS9u3bR9OmTfm///s/3n//fZtt3NzceO+992jbti3t2rXj7NmzrFixAq1Wi6enJ/PmzaNLly40b96cdevW8ccff+Dj43PFY3/77bcEBATQvXt3hgwZwtixY3Fzc8PR0fF6vd2bmkaxTHa/SVimIGdmZuLu7l7dzRFCiEopKCggJiaGunXrSodUgbfeeouff/6Zw4cPV3dTbpjz588TEhLCunXr6NOnT3U351+73Pf8WvpvGe4RQghRrXJycjh79iyffvop06ZNq+7mXFcbNmwgJyeHZs2akZCQwAsvvEBYWBjdu3ev7qbV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", "text/plain": [ "
" ] @@ -664,12 +674,17 @@ } ], "source": [ - "df.plot(title=\"Numeric features\")" + "(\n", + " df\n", + " .sort_values(\"body_mass_g\")\n", + " .reset_index(drop=True)\n", + " .plot(title=\"Numeric features\", logy=True)\n", + ")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -678,13 +693,13 @@ "" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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bN68udwdkp48eTLS0tIUX0lJSe/dtmiMw7t3RCaqaIo+3xzTQ0QV0ScdgYmOjsaTJ0+U7h+Tn5+P06dPY+nSpThy5AhycnKQmpqqdBTm8ePHsLCwAABYWFggMjJSab9FVym9u83fr1x6/Pgx5HL5eydV09bWhra29ke9D3V1dRgZGSnux6Onp8fJy6jCKCwsRGZmJp48eQIjIyOoq6sLHYmIqMR9UoFp27atyhTtQ4YMQc2aNREYGAhra2toamoiLCxMcYfhuLg4JCYmKm4a6Onpiblz5+LJkyeK8RShoaGQy+WKKe89PT1x8OBBpdcJDQ1VufHgf1FUlt69qSBRRWJkZKT4nBMRVTSfVGAMDAxQu3ZtpWWVKlWCiYmJYrmfnx/Gjx+PypUrQy6XY9SoUfD09ESTJk0AAO3bt4erqysGDhyIoKAgpKSkYMqUKRg5cqTiCMrw4cOxdOlSTJo0Cb6+vjh+/Di2b9+OAwcOlMR7BvD2hoKWlpYwMzPjjf+owtHU1OSRFyKq0Er8VgKLFi2CmpoaevXqhezsbHh7e2P58uWK9erq6ti/fz9GjBgBT09PVKpUCYMGDcKsWbMU29jb2+PAgQMYN24cFi9ejGrVqmHNmjXw9vYu6bhQV1fnD3oiIiKRkeStBIiIiMSA88CU0q0EiIiIiITAAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiU+I3cyQiEquSvu9Meb/nDJGY8QgMERERiQ6PwJAgpHiHVSIiKjk8AkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDRERE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", + "image/png": "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GGzevLncHZKePHky0tLSFF9JSUnv3bZojMO7d0QmqmiKPt8c00NEFdEnHYGJjo7GkydPlO4fk5+fj9OnT2Pp0qU4cuQIcnJykJqaqnQU5vHjx7CwsAAAWFhYIDIyUmm/RVcpvbvN369cevz4MeRy+XsnVdPW1oa2tvZHvQ91dXUYGRkp7sejp6fHycuowigsLERmZiaePHkCIyMjqKurCx2JiKjEfVKBadu2rcoU7UOGDEHNmjURGBgIa2traGpqIiwsTHGH4bi4OCQmJipuGujp6Ym5c+fiyZMnivEUoaGhkMvliinvPT09cfDgQaXXCQ0NVbnx4H9RVJbevakgUUViZGSk+JwTEVU0n1RgDAwMULt2baVllSpVgomJiWK5n58fxo8fj8qVK0Mul2PUqFHw9PREkyZNAADt27eHq6srBg4ciKCgIKSkpGDKlCkYOXKk4gjK8OHDsXTpUkyaNAm+vr44fvw4tm/fjgMHDpTEewbw9oaClpaWMDMz443/qMLR1NTkkRciqtBK/FYCixYtgpqaGnr16oXs7Gx4e3tj+fLlivXq6urYv38/RowYAU9PT1SqVAmDBg3CrFmzFNvY29vjwIEDGDduHBYvXoxq1aphzZo18Pb2Lum4UFdX5w96IiIikZHkrQSIiIjEgPPAlNKtBIiIiIiEwAJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREolPiN3MkIhKrkr7vTHm/5wyRmPEIDBEREYkOj8CQIKR4h1UiIio5PAJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RERKLDAkNERESiwwJDREREosMCQ0RE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", "text/plain": [ "
" ] @@ -709,12 +724,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAyUAAAGFCAYAAADjF1xYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABqp0lEQVR4nO3dd1hTZ/8G8DsJe++hoogg4gBxj9aFCq5WX1utWqvVumfd/mytVm21jlbraGutWut+bau11l19FScquBARwQkiU3Ygye8PampkCDKehNyf6+LS5Jw85z4hQL55xpGoVCoViIiIiIiIBJGKDkBERERERPqNRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJERELNmzcPjRs3LvH+MTExkEgkCA0NBQCcOHECEokEKSkpFZJP23To0AGTJ08uczuJiYlwcnJCTExMmdvSJRKJBL///juAgq+lyvA6x3z5e+7u7o5vvvmmXHO1atUKe/bsKdc2iUqDRQkREZWrs2fPQiaToUePHpVyvDZt2iA2NhbW1tav3camTZsgkUggkUgglUpRo0YNfPjhh4iPjy/HpOXj119/xYIFC8rczqJFi/D222/D3d0dwL9vlp9/2dvbo2vXrrhy5UqZj6Wt3NzcEBsbi4YNG4qOUioXL17EyJEjy7XNTz75BLNmzYJSqSzXdolKikUJERGVqw0bNmDChAn43//+h8ePH1f48YyMjODi4gKJRFKmdqysrBAbG4uHDx9i/fr1+OuvvzB48OBySll+7OzsYGlpWaY2MjMzsWHDBgwfPrzAtqNHjyI2NhaHDh1Ceno6unXrVmV7oWQyGVxcXGBgYCA6Sqk4OjrCzMysXNvs1q0b0tLS8Ndff5Vru0QlxaKEiIjKTXp6Onbu3IkxY8agR48e2LRpU4F9Fi9e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VSVKRX6holLmFx9SqehEREREr8SihIiIiIiIhOJHaEREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREL9P6MtWHxUCA+9AAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAyUAAAGFCAYAAADjF1xYAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaqdJREFUeJzt3XdYU2f/BvA7CXvvoaKIIOIAcY/WhQquVl9brVqr1bpn3f5srVZttY5W62hrrVrrfm2rtdZdfRUnKrgQEcEJIlN2IMnvD2pqZAgynoTcn+vi0uScPOc+IUC+ecaRqFQqFYiIiIiIiASRig5ARERERET6jUUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERCzZs3D40bNy7x/jExMZBIJAgNDQUAnDhxAhKJBCkpKRWST9t06NABkydPLnM7iYmJcHJyQkxMTJnb0iUSiQS///47gIKvpcrwOsd8+Xvu7u6Ob775plxztWrVCnv27CnXNolKg0UJERGVq7Nnz0Imk6FHjx6Vcrw2bdogNjYW1tbWr93Gpk2bIJFIIJFIIJVKUaNGDXz44YeIj48vx6Tl49dff8WCBQvK3M6iRYvw9ttvw93dHcC/b5aff9nb26Nr1664cuVKmY+lrdzc3BAbG4uGDRuKjlIqFy9exMiRI8u1zU8++QSzZs2CUqks13aJSopFCRERlasNGzZgwoQJ+N///ofHjx9X+PGMjIzg4uICiURSpnasrKwQGxuLhw8fYv369fjrr78wePDgckpZfuzs7GBpaVmmNjIzM7FhwwYMHz68wLajR48iNjYWhw4dQnp6Orp161Zle6FkMhlcXFxgYGAgOkqpODo6wszMrFzb7NatG9LS0vDXX3+Va7tEJcWihIiIyk16ejp27tyJMWPGoEePHti0aVOBfRYv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EVUlSkV+oaJS5hcfUqnoRERERK/EooSIiIiIiITiR2hERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERCsSghIiIiIiKhWJQQEREREZFQLEqIiIiIiEgoFiVERERERCQUixIiIiIiIhKKRQkREREREQnFooSIiIiIiIRiUUJEREREREKxKCEiIiIiIqFYlBARERERkVAsSoiIiIiISCgWJUREREREJBSLEiIiIiIiEopFCRERERERCcWihIiIiIiIhGJRQkREREREQrEoISIiIiIioViUEBERERGRUCxKiIiIiIhIKBYlREREREQkFIsSIiIiIiISikUJEREREREJxaKEiIiIiIiEYlFCRERERERC/T+jLVh8VAgPvQAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -747,7 +762,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -801,47 +816,67 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# Drop any rows that has missing (NA) values\n", + "df = df.dropna()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Part of preparing data for a machine learning task is splitting it into subsets for training and testing to ensure that the solution is not overfitting. By default, BQML will automatically manage splitting the data for you. However, BQML also supports manually splitting out your training data.\n", + "\n", + "Performing a manual data split can be done with `bigframes.ml.model_selection.train_test_split` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:263: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n", + " warnings.warn(msg, category=bfe.AmbiguousWindowWarning)\n", + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:239: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n", + " warnings.warn(msg, bfe.AmbiguousWindowWarning)\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ "\n", - " X shape: (334, 6)\n", - " y shape: (334, 1)\n", + " df_train shape: (267, 7)\n", + " df_test shape: (67, 7)\n", "\n" ] } ], "source": [ - "# Drop any rows that has missing (NA) values\n", - "df = df.dropna()\n", + "from bigframes.ml.model_selection import train_test_split\n", "\n", - "# Isolate input features and output variable into DataFrames\n", - "X = df[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex', 'species']]\n", - "y = df[['body_mass_g']]\n", "\n", - "# Print the shapes of features and label\n", + "# This will split df into test and training sets, with 20% of the rows in the test set,\n", + "# and the rest in the training set\n", + "df_train, df_test = train_test_split(df, test_size=0.2)\n", + "\n", + "# Show the shape of the data after the split\n", "print(f\"\"\"\n", - " X shape: {X.shape}\n", - " y shape: {y.shape}\n", + " df_train shape: {df_train.shape}\n", + " df_test shape: {df_test.shape}\n", "\"\"\")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Part of preparing data for a machine learning task is splitting it into subsets for training and testing to ensure that the solution is not overfitting. By default, BQML will automatically manage splitting the data for you. However, BQML also supports manually splitting out your training data.\n", - "\n", - "Performing a manual data split can be done with `bigframes.ml.model_selection.train_test_split` like so:" - ] - }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -850,7 +885,7 @@ "text": [ "\n", " X_train shape: (267, 6)\n", - " X_test shape: (67, 6)\n", + " X_test shape: (67, 7)\n", " y_train shape: (267, 1)\n", " y_test shape: (67, 1)\n", "\n" @@ -858,13 +893,31 @@ } ], "source": [ - "from bigframes.ml.model_selection import train_test_split\n", + "# Isolate input features and output variable into DataFrames\n", + "X_train = df_train[[\n", + " 'island',\n", + " 'culmen_length_mm',\n", + " 'culmen_depth_mm',\n", + " 'flipper_length_mm',\n", + " 'sex',\n", + " 'species',\n", + "]]\n", + "y_train = df_train[['body_mass_g']]\n", "\n", - "# This will split X and y into test and training sets, with 20% of the rows in the test set,\n", - "# and the rest in the training set\n", - "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n", + "X_test = df_test[[\n", + " 'island',\n", + " 'culmen_length_mm',\n", + " 'culmen_depth_mm',\n", + " 'flipper_length_mm',\n", + " 'sex',\n", + " 'species',\n", + " # Include the actual body_mass_g so that we can compare with the predicted\n", + " # without a join.\n", + " 'body_mass_g'\n", + "]]\n", + "y_test = df_test[['body_mass_g']]\n", "\n", - "# Show the shape of the data after the split\n", + "# Print the shapes of features and label\n", "print(f\"\"\"\n", " X_train shape: {X_train.shape}\n", " X_test shape: {X_test.shape}\n", @@ -885,7 +938,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -901,7 +954,7 @@ " ('linreg', LinearRegression(fit_intercept=False))])" ] }, - "execution_count": 18, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -939,7 +992,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -970,80 +1023,86 @@ " flipper_length_mm\n", " sex\n", " species\n", + " body_mass_g\n", " \n", " \n", " \n", " \n", - " 37\n", - " -18640.718256\n", - " Biscoe\n", - " 44.5\n", - " 15.7\n", - " 217.0\n", - " .\n", - " Gentoo penguin (Pygoscelis papua)\n", - " \n", - " \n", - " 245\n", - " 3109.962252\n", + " 0\n", + " 3444.135246\n", " Dream\n", - " 33.1\n", - " 16.1\n", - " 178.0\n", + " 42.2\n", + " 18.5\n", + " 180.0\n", " FEMALE\n", " Adelie Penguin (Pygoscelis adeliae)\n", + " 3550.0\n", " \n", " \n", - " 267\n", - " 3372.443434\n", + " 1\n", + " 3735.564386\n", " Torgersen\n", - " 41.1\n", - " 17.6\n", - " 182.0\n", - " FEMALE\n", + " 39.1\n", + " 18.7\n", + " 181.0\n", + " MALE\n", " Adelie Penguin (Pygoscelis adeliae)\n", + " 3750.0\n", " \n", " \n", - " 280\n", - " 3341.376012\n", - " Torgersen\n", - " 36.6\n", - " 17.8\n", - " 185.0\n", - " FEMALE\n", + " 2\n", + " 3879.370094\n", + " Dream\n", + " 40.9\n", + " 18.9\n", + " 184.0\n", + " MALE\n", " Adelie Penguin (Pygoscelis adeliae)\n", + " 3900.0\n", " \n", " \n", - " 40\n", - " 3310.178937\n", + " 3\n", + " 3787.401253\n", " Biscoe\n", - " 37.6\n", - " 17.0\n", + " 38.2\n", + " 18.1\n", " 185.0\n", + " MALE\n", + " Adelie Penguin (Pygoscelis adeliae)\n", + " 3950.0\n", + " \n", + " \n", + " 4\n", + " 3435.804331\n", + " Dream\n", + " 36.0\n", + " 18.5\n", + " 186.0\n", " FEMALE\n", " Adelie Penguin (Pygoscelis adeliae)\n", + " 3100.0\n", " \n", " \n", "\n", "" ], "text/plain": [ - " predicted_body_mass_g island culmen_length_mm culmen_depth_mm \\\n", - "37 -18640.718256 Biscoe 44.5 15.7 \n", - "245 3109.962252 Dream 33.1 16.1 \n", - "267 3372.443434 Torgersen 41.1 17.6 \n", - "280 3341.376012 Torgersen 36.6 17.8 \n", - "40 3310.178937 Biscoe 37.6 17.0 \n", + " predicted_body_mass_g island culmen_length_mm culmen_depth_mm \\\n", + "0 3444.135246 Dream 42.2 18.5 \n", + "1 3735.564386 Torgersen 39.1 18.7 \n", + "2 3879.370094 Dream 40.9 18.9 \n", + "3 3787.401253 Biscoe 38.2 18.1 \n", + "4 3435.804331 Dream 36.0 18.5 \n", "\n", - " flipper_length_mm sex species \n", - "37 217.0 . Gentoo penguin (Pygoscelis papua) \n", - "245 178.0 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "267 182.0 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "280 185.0 FEMALE Adelie Penguin (Pygoscelis adeliae) \n", - "40 185.0 FEMALE Adelie Penguin (Pygoscelis adeliae) " + " flipper_length_mm sex species body_mass_g \n", + "0 180.0 FEMALE Adelie Penguin (Pygoscelis adeliae) 3550.0 \n", + "1 181.0 MALE Adelie Penguin (Pygoscelis adeliae) 3750.0 \n", + "2 184.0 MALE Adelie Penguin (Pygoscelis adeliae) 3900.0 \n", + "3 185.0 MALE Adelie Penguin (Pygoscelis adeliae) 3950.0 \n", + "4 186.0 FEMALE Adelie Penguin (Pygoscelis adeliae) 3100.0 " ] }, - "execution_count": 19, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1070,7 +1129,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -1105,12 +1164,12 @@ " \n", " \n", " 0\n", - " 582.272638\n", - " 8337651.200465\n", - " 0.004989\n", - " 193.446297\n", - " -11.273389\n", - " -11.091156\n", + " 212.800303\n", + " 72655.272611\n", + " 0.004369\n", + " 144.426983\n", + " 0.877546\n", + " 0.877991\n", " \n", " \n", "\n", @@ -1118,22 +1177,22 @@ "[1 rows x 6 columns in total]" ], "text/plain": [ - " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 582.272638 8337651.200465 0.004989 \n", + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 212.800303 72655.272611 0.004369 \n", "\n", - " median_absolute_error r2_score explained_variance \n", - "0 193.446297 -11.273389 -11.091156 \n", + " median_absolute_error r2_score explained_variance \n", + " 144.426983 0.877546 0.877991 \n", "\n", "[1 rows x 6 columns]" ] }, - "execution_count": 20, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pipeline.score(X_test, y_test)" + "pipeline.score(X_test.drop(columns=[\"body_mass_g\"]), y_test)" ] }, { @@ -1145,16 +1204,16 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "-11.273389374372979" + "np.float64(0.8775458183087934)" ] }, - "execution_count": 21, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -1162,7 +1221,7 @@ "source": [ "from bigframes.ml.metrics import r2_score\n", "\n", - "r2_score(y_test, y_pred[\"predicted_body_mass_g\"])" + "r2_score(y_pred['body_mass_g'], y_pred[\"predicted_body_mass_g\"])" ] }, { @@ -1187,7 +1246,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -1241,17 +1300,17 @@ "[3 rows x 1 columns]" ] }, - "execution_count": 22, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# df = bpd.DataFrame(\n", - "# {\n", - "# \"prompt\": [\"What is BigQuery?\", \"What is BQML?\", \"What is BigQuery DataFrames?\"],\n", - "# })\n", - "# df" + "df = bpd.DataFrame(\n", + " {\n", + " \"prompt\": [\"What is BigQuery?\", \"What is BQML?\", \"What is BigQuery DataFrames?\"],\n", + " })\n", + "df" ] }, { @@ -1265,15 +1324,18 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/shobs/code/bigframes/bigframes/core/__init__.py:114: PreviewWarning: Interpreting JSON column(s) as pyarrow.large_string. This behavior may change in future versions.\n", - " warnings.warn(msg, bfe.PreviewWarning)\n" + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:109: PreviewWarning: JSON column interpretation as a custom PyArrow extention in\n", + "`db_dtypes` is a preview feature and subject to change.\n", + " warnings.warn(msg, bfe.PreviewWarning)\n", + "/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/array_value.py:263: AmbiguousWindowWarning: Window ordering may be ambiguous, this can cause unstable results.\n", + " warnings.warn(msg, category=bfe.AmbiguousWindowWarning)\n" ] }, { @@ -1305,28 +1367,23 @@ " \n", " \n", " \n", - " 0\n", - " ## BigQuery: Your Data Warehouse in the Cloud\n", - "...\n", - " [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob...\n", + " 1\n", + " BQML stands for **BigQuery Machine Learning**....\n", + " <NA>\n", " \n", - " What is BigQuery?\n", + " What is BQML?\n", " \n", " \n", - " 1\n", - " ## BQML - BigQuery Machine Learning\n", - "\n", - "BQML stan...\n", - " [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob...\n", + " 0\n", + " BigQuery is a fully managed, serverless data w...\n", + " <NA>\n", " \n", - " What is BQML?\n", + " What is BigQuery?\n", " \n", " \n", " 2\n", - " ## BigQuery DataFrames\n", - "\n", - "BigQuery DataFrames is...\n", - " [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob...\n", + " BigQuery DataFrames are a Python library that ...\n", + " <NA>\n", " \n", " What is BigQuery DataFrames?\n", " \n", @@ -1337,29 +1394,24 @@ ], "text/plain": [ " ml_generate_text_llm_result \\\n", - "0 ## BigQuery: Your Data Warehouse in the Cloud\n", - "... \n", - "1 ## BQML - BigQuery Machine Learning\n", - "\n", - "BQML stan... \n", - "2 ## BigQuery DataFrames\n", + "1 BQML stands for **BigQuery Machine Learning**.... \n", + "0 BigQuery is a fully managed, serverless data w... \n", + "2 BigQuery DataFrames are a Python library that ... \n", "\n", - "BigQuery DataFrames is... \n", - "\n", - " ml_generate_text_rai_result ml_generate_text_status \\\n", - "0 [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob... \n", - "1 [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob... \n", - "2 [{\"category\":\"HARM_CATEGORY_HATE_SPEECH\",\"prob... \n", + " ml_generate_text_rai_result ml_generate_text_status \\\n", + "1 \n", + "0 \n", + "2 \n", "\n", " prompt \n", - "0 What is BigQuery? \n", "1 What is BQML? \n", + "0 What is BigQuery? \n", "2 What is BigQuery DataFrames? \n", "\n", "[3 rows x 4 columns]" ] }, - "execution_count": 23, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -1382,49 +1434,44 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "## BigQuery DataFrames\n", - "\n", - "BigQuery DataFrames is a Python library that allows you to interact with BigQuery data using the familiar Pandas API. This means you can use all the powerful tools and methods from the Pandas library to explore, analyze, and transform your BigQuery data, without needing to learn a new language or API.\n", - "\n", - "Here are some of the key benefits of using BigQuery DataFrames:\n", - "\n", - "* **Ease of use:** If you're already familiar with Pandas, you can start using BigQuery DataFrames with minimal learning curve.\n", - "* **Speed and efficiency:** BigQuery DataFrames leverages the power of BigQuery to perform complex operations on large datasets efficiently.\n", - "* **Flexibility:** You can use BigQuery DataFrames for a wide range of tasks, including data exploration, analysis, cleaning, and transformation.\n", - "* **Integration with other tools:** BigQuery DataFrames integrates seamlessly with other Google Cloud tools like Colab and Vertex AI, allowing you to build end-to-end data analysis pipelines.\n", - "\n", - "Here are some of the key features of BigQuery DataFrames:\n", - "\n", - "* **Support for most Pandas operations:** You can use most of the DataFrame methods you're familiar with, such as `groupby`, `filter`, `sort_values`, and `apply`.\n", - "* **Automatic schema inference:** BigQuery DataFrames automatically infers the schema of your data, so you don't need to manually specify it.\n", - "* **Efficient handling of large datasets:** BigQuery DataFrames pushes computations to BigQuery, which allows you to work with large datasets without running out of memory.\n", - "* **Support for both public and private datasets:** You can use BigQuery DataFrames to access both public and private datasets stored in BigQuery.\n", - "\n", - "## Getting Started with BigQuery DataFrames\n", - "\n", - "Getting started with BigQuery DataFrames is easy. You just need to install the library and configure your authentication. Once you're set up, you can start using it to interact with your BigQuery data.\n", - "\n", - "Here are some resources to help you get started:\n", - "\n", - "* **Documentation:** https://cloud.google.com/bigquery/docs/reference/libraries/bigquery-dataframe\n", - "* **Quickstart:** https://cloud.google.com/bigquery/docs/reference/libraries/bigquery-dataframe-python-quickstart\n", - "* **Tutorials:** https://cloud.google.com/bigquery/docs/tutorials/bq-dataframe-pandas-tutorial\n", - "\n", - "## Conclusion\n", - "\n", - "BigQuery DataFrames is a powerful tool that can help you get the most out of your BigQuery data. If you're looking for a way to easily analyze and transform your BigQuery data using the familiar Pandas API, then BigQuery DataFrames is a great option.\n" - ] + "data": { + "text/markdown": [ + "BigQuery DataFrames are a Python library that provides a Pandas-like interface for interacting with BigQuery data. Instead of loading entire datasets into memory (which is impossible for very large BigQuery tables), BigQuery DataFrames allow you to work with BigQuery data in a way that feels familiar if you've used Pandas, but leverages BigQuery's processing power for efficiency. This means you can perform data analysis and manipulation on datasets that are too large for Pandas to handle directly.\n", + "\n", + "Key features and characteristics include:\n", + "\n", + "* **Lazy Evaluation:** BigQuery DataFrames don't load the entire dataset into memory. Operations are expressed as queries that are executed in BigQuery only when necessary (e.g., when you call `.to_dataframe()` to materialize a result, or when you explicitly trigger execution). This significantly reduces memory consumption.\n", + "\n", + "* **Pandas-like API:** The library aims for a familiar API similar to Pandas. You can use many of the same functions and methods you would use with Pandas DataFrames, such as filtering, selecting columns, aggregations, and joining.\n", + "\n", + "* **Integration with BigQuery:** The library seamlessly integrates with BigQuery. It allows you to read data from BigQuery tables and write data back to BigQuery.\n", + "\n", + "* **Scalability:** Because the processing happens in BigQuery, BigQuery DataFrames can scale to handle datasets of virtually any size. It's designed to efficiently process terabytes or even petabytes of data.\n", + "\n", + "* **Performance:** While providing a user-friendly interface, BigQuery DataFrames leverages BigQuery's optimized query engine for fast execution of operations.\n", + "\n", + "* **SQL integration:** While providing a Pythonic interface, you can easily incorporate SQL queries directly within the DataFrame operations providing flexibility and control over the data manipulation.\n", + "\n", + "\n", + "**In short:** BigQuery DataFrames provide a powerful and efficient way to work with large BigQuery datasets using a familiar Pandas-like syntax without the memory limitations of loading the entire dataset into local memory. They bridge the gap between the ease of use of Pandas and the scalability of BigQuery.\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# print(pred.loc[2][\"ml_generate_text_llm_result\"])" + "# import IPython.display\n", + "\n", + "# IPython.display.Markdown(pred.loc[2][\"ml_generate_text_llm_result\"])" ] }, { @@ -1443,7 +1490,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -1491,7 +1538,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.6" } }, "nbformat": 4, From 3eadf75defd71627ea98c8073eee669a5da3c9f8 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Tue, 29 Apr 2025 09:33:41 -0700 Subject: [PATCH 20/28] refactor: add compile_readlocal for SQLGlotCompiler (#1663) This change is adding compile_readlocal for SQLGlotCompiler class and also introducing snapshot as dev dependency for unit tests. --- bigframes/core/compile/compiled.py | 8 +- bigframes/core/compile/compiler.py | 4 +- bigframes/core/compile/sqlglot/compiler.py | 59 +++++++--- bigframes/core/compile/sqlglot/sql_gen.py | 38 ------- bigframes/core/compile/sqlglot/sqlglot_ir.py | 103 ++++++++++++++++++ .../core/compile/sqlglot/sqlglot_types.py | 2 +- bigframes/core/nodes.py | 4 +- noxfile.py | 4 +- setup.py | 3 +- tests/unit/core/compile/sqlglot/conftest.py | 13 ++- .../test_compile_readlocal/out.sql | 3 + .../compile/sqlglot/test_compile_readlocal.py | 10 +- 12 files changed, 176 insertions(+), 75 deletions(-) delete mode 100644 bigframes/core/compile/sqlglot/sql_gen.py create mode 100644 bigframes/core/compile/sqlglot/sqlglot_ir.py create mode 100644 tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index a79ad9fe55..314b54fc6d 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -288,13 +288,7 @@ def _reproject_to_table(self) -> UnorderedIR: def from_polars( cls, pa_table: pa.Table, schema: Sequence[bigquery.SchemaField] ) -> UnorderedIR: - # TODO: add offsets - """ - Builds an in-memory only (SQL only) expr from a pandas dataframe. - - Assumed that the dataframe has unique string column names and bigframes-suppported - dtypes. - """ + """Builds an in-memory only (SQL only) expr from a pyarrow table.""" import bigframes_vendored.ibis.backends.bigquery.datatypes as third_party_ibis_bqtypes # derive the ibis schema from the original pandas schema diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index a778889925..21045ca348 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -22,7 +22,6 @@ import bigframes_vendored.ibis.expr.api as ibis_api import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes import bigframes_vendored.ibis.expr.types as ibis_types -import google.cloud.bigquery import pyarrow as pa from bigframes import dtypes, operations @@ -169,7 +168,7 @@ def compile_readlocal(node: nodes.ReadLocalNode, *args): pa_table = node.local_data_source.data bq_schema = node.schema.to_bigquery() - pa_table = pa_table.select(list(item.source_id for item in node.scan_list.items)) + pa_table = pa_table.select([item.source_id for item in node.scan_list.items]) pa_table = pa_table.rename_columns( {item.source_id: item.id.sql for item in node.scan_list.items} ) @@ -178,7 +177,6 @@ def compile_readlocal(node: nodes.ReadLocalNode, *args): pa_table = pa_table.append_column( offsets, pa.array(range(pa_table.num_rows), type=pa.int64()) ) - bq_schema = (*bq_schema, google.cloud.bigquery.SchemaField(offsets, "INT64")) return compiled.UnorderedIR.from_polars(pa_table, bq_schema) diff --git a/bigframes/core/compile/sqlglot/compiler.py b/bigframes/core/compile/sqlglot/compiler.py index df800c5489..2b78a6e458 100644 --- a/bigframes/core/compile/sqlglot/compiler.py +++ b/bigframes/core/compile/sqlglot/compiler.py @@ -15,14 +15,15 @@ import dataclasses import functools +import itertools import typing -import google.cloud.bigquery as bigquery -import sqlglot.expressions as sge +from google.cloud import bigquery +import pyarrow as pa -from bigframes.core import expression, nodes, rewrite +from bigframes.core import expression, identifiers, nodes, rewrite from bigframes.core.compile import configs -from bigframes.core.compile.sqlglot import sql_gen +import bigframes.core.compile.sqlglot.sqlglot_ir as ir import bigframes.core.ordering as bf_ordering @@ -30,8 +31,6 @@ class SQLGlotCompiler: """Compiles BigFrame nodes into SQL using SQLGlot.""" - sql_gen = sql_gen.SQLGen() - def compile( self, node: nodes.BigFrameNode, @@ -81,6 +80,7 @@ def _compile_sql(self, request: configs.CompileRequest) -> configs.CompileResult result_node = typing.cast( nodes.ResultNode, rewrite.column_pruning(result_node) ) + result_node = _remap_variables(result_node) sql = self._compile_result_node(result_node) return configs.CompileResult( sql, result_node.schema.to_bigquery(), result_node.order_by @@ -89,6 +89,8 @@ def _compile_sql(self, request: configs.CompileRequest) -> configs.CompileResult ordering: typing.Optional[bf_ordering.RowOrdering] = result_node.order_by result_node = dataclasses.replace(result_node, order_by=None) result_node = typing.cast(nodes.ResultNode, rewrite.column_pruning(result_node)) + + result_node = _remap_variables(result_node) sql = self._compile_result_node(result_node) # Return the ordering iff no extra columns are needed to define the row order if ordering is not None: @@ -103,9 +105,9 @@ def _compile_sql(self, request: configs.CompileRequest) -> configs.CompileResult ) def _compile_result_node(self, root: nodes.ResultNode) -> str: - sqlglot_expr = compile_node(root.child) + sqlglot_ir = compile_node(root.child) # TODO: add order_by, limit, and selections to sqlglot_expr - return self.sql_gen.sql(sqlglot_expr) + return sqlglot_ir.sql def _replace_unsupported_ops(node: nodes.BigFrameNode): @@ -115,27 +117,52 @@ def _replace_unsupported_ops(node: nodes.BigFrameNode): return node +def _remap_variables(node: nodes.ResultNode) -> nodes.ResultNode: + """Remaps `ColumnId`s in the BFET of a `ResultNode` to produce deterministic UIDs.""" + + def anonymous_column_ids() -> typing.Generator[identifiers.ColumnId, None, None]: + for i in itertools.count(): + yield identifiers.ColumnId(name=f"bfcol_{i}") + + result_node, _ = rewrite.remap_variables(node, anonymous_column_ids()) + return typing.cast(nodes.ResultNode, result_node) + + @functools.lru_cache(maxsize=5000) -def compile_node(node: nodes.BigFrameNode) -> sge.Expression: - """Compile node into CompileArrayValue. Caches result.""" +def compile_node(node: nodes.BigFrameNode) -> ir.SQLGlotIR: + """Compiles node into CompileArrayValue. Caches result.""" return node.reduce_up(lambda node, children: _compile_node(node, *children)) @functools.singledispatch def _compile_node( - node: nodes.BigFrameNode, *compiled_children: sge.Expression -) -> sge.Expression: + node: nodes.BigFrameNode, *compiled_children: ir.SQLGlotIR +) -> ir.SQLGlotIR: """Defines transformation but isn't cached, always use compile_node instead""" raise ValueError(f"Can't compile unrecognized node: {node}") @_compile_node.register -def compile_readlocal(node: nodes.ReadLocalNode, *args) -> sge.Expression: - # TODO: add support for reading from local files - return sge.select() +def compile_readlocal(node: nodes.ReadLocalNode, *args) -> ir.SQLGlotIR: + offsets = node.offsets_col.sql if node.offsets_col else None + schema_names = node.schema.names + schema_dtypes = node.schema.dtypes + + pa_table = node.local_data_source.data + pa_table = pa_table.select([item.source_id for item in node.scan_list.items]) + pa_table = pa_table.rename_columns( + {item.source_id: item.id.sql for item in node.scan_list.items} + ) + + if offsets: + pa_table = pa_table.append_column( + offsets, pa.array(range(pa_table.num_rows), type=pa.int64()) + ) + + return ir.SQLGlotIR.from_pandas(pa_table.to_pandas(), schema_names, schema_dtypes) @_compile_node.register -def compile_selection(node: nodes.SelectionNode, child: sge.Expression): +def compile_selection(node: nodes.SelectionNode, child: ir.SQLGlotIR): # TODO: add support for selection return child diff --git a/bigframes/core/compile/sqlglot/sql_gen.py b/bigframes/core/compile/sqlglot/sql_gen.py deleted file mode 100644 index 85784e3f5d..0000000000 --- a/bigframes/core/compile/sqlglot/sql_gen.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import dataclasses - -import sqlglot.dialects.bigquery -import sqlglot.expressions as sge - - -@dataclasses.dataclass(frozen=True) -class SQLGen: - """Helper class to build SQLGlot Query and generate SQL string.""" - - dialect = sqlglot.dialects.bigquery.BigQuery - """The SQL dialect used for generation.""" - - quoted: bool = True - """Whether to quote identifiers in the generated SQL.""" - - pretty: bool = True - """Whether to pretty-print the generated SQL.""" - - def sql(self, expr: sge.Expression) -> str: - """Generate SQL string from the given expression.""" - return expr.sql(dialect=self.dialect, pretty=self.pretty) diff --git a/bigframes/core/compile/sqlglot/sqlglot_ir.py b/bigframes/core/compile/sqlglot/sqlglot_ir.py new file mode 100644 index 0000000000..836e7f17ba --- /dev/null +++ b/bigframes/core/compile/sqlglot/sqlglot_ir.py @@ -0,0 +1,103 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import typing + +import pandas as pd +import sqlglot as sg +import sqlglot.dialects.bigquery +import sqlglot.expressions as sge + +from bigframes import dtypes +import bigframes.core.compile.sqlglot.sqlglot_types as sgt + + +@dataclasses.dataclass(frozen=True) +class SQLGlotIR: + """Helper class to build SQLGlot Query and generate SQL string.""" + + expr: sge.Expression = sge.Expression() + """The SQLGlot expression representing the query.""" + + dialect = sqlglot.dialects.bigquery.BigQuery + """The SQL dialect used for generation.""" + + quoted: bool = True + """Whether to quote identifiers in the generated SQL.""" + + pretty: bool = True + """Whether to pretty-print the generated SQL.""" + + @property + def sql(self) -> str: + """Generate SQL string from the given expression.""" + return self.expr.sql(dialect=self.dialect, pretty=self.pretty) + + @classmethod + def from_pandas( + cls, + pd_df: pd.DataFrame, + schema_names: typing.Sequence[str], + schema_dtypes: typing.Sequence[dtypes.Dtype], + ) -> SQLGlotIR: + """Builds SQLGlot expression from pyarrow table.""" + dtype_expr = sge.DataType( + this=sge.DataType.Type.STRUCT, + expressions=[ + sge.ColumnDef( + this=sge.to_identifier(name, quoted=True), + kind=sgt.SQLGlotType.from_bigframes_dtype(dtype), + ) + for name, dtype in zip(schema_names, schema_dtypes) + ], + nested=True, + ) + data_expr = [ + sge.Tuple( + expressions=tuple( + _literal( + value=value, + dtype=sgt.SQLGlotType.from_bigframes_dtype(dtype), + ) + for value, dtype in zip(row, schema_dtypes) + ) + ) + for _, row in pd_df.iterrows() + ] + expr = sge.Unnest( + expressions=[ + sge.DataType( + this=sge.DataType.Type.ARRAY, + expressions=[dtype_expr], + nested=True, + values=data_expr, + ), + ], + ) + return cls(expr=sg.select(sge.Star()).from_(expr)) + + +def _literal(value: typing.Any, dtype: str) -> sge.Expression: + if value is None: + return _cast(sge.Null(), dtype) + + # TODO: handle other types like visit_DefaultLiteral + return sge.convert(value) + + +def _cast(arg, to) -> sge.Cast: + return sge.Cast(this=arg, to=to) diff --git a/bigframes/core/compile/sqlglot/sqlglot_types.py b/bigframes/core/compile/sqlglot/sqlglot_types.py index 06c78c1435..0cfeaae3e9 100644 --- a/bigframes/core/compile/sqlglot/sqlglot_types.py +++ b/bigframes/core/compile/sqlglot/sqlglot_types.py @@ -32,7 +32,7 @@ def from_bigframes_dtype( bigframes_dtype: typing.Union[ bigframes.dtypes.DtypeString, bigframes.dtypes.Dtype, np.dtype[typing.Any] ], - ): + ) -> str: if bigframes_dtype == bigframes.dtypes.INT_DTYPE: return "INT64" elif bigframes_dtype == bigframes.dtypes.FLOAT_DTYPE: diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index f7327f2a7a..558e4a2aaf 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -1636,11 +1636,11 @@ def remap_vars( def remap_refs( self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] ) -> ResultNode: - output_names = tuple( + output_cols = tuple( (ref.remap_column_refs(mappings), name) for ref, name in self.output_cols ) order_by = self.order_by.remap_column_refs(mappings) if self.order_by else None - return dataclasses.replace(self, output_names=output_names, order_by=order_by) # type: ignore + return dataclasses.replace(self, output_cols=output_cols, order_by=order_by) # type: ignore @property def consumed_ids(self) -> COLUMN_SET: diff --git a/noxfile.py b/noxfile.py index b29cda7a51..3bdff699ed 100644 --- a/noxfile.py +++ b/noxfile.py @@ -76,7 +76,7 @@ ] UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] UNIT_TEST_DEPENDENCIES: List[str] = [] -UNIT_TEST_EXTRAS: List[str] = [] +UNIT_TEST_EXTRAS: List[str] = ["tests"] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = { "3.12": ["polars", "scikit-learn"], } @@ -203,7 +203,7 @@ def install_unittest_dependencies(session, install_test_extra, *constraints): if install_test_extra and UNIT_TEST_EXTRAS_BY_PYTHON: extras = UNIT_TEST_EXTRAS_BY_PYTHON.get(session.python, []) - elif install_test_extra and UNIT_TEST_EXTRAS: + if install_test_extra and UNIT_TEST_EXTRAS: extras = UNIT_TEST_EXTRAS else: extras = [] diff --git a/setup.py b/setup.py index d0ee4da405..489d9aacd9 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ ] extras = { # Optional test dependencies packages. If they're missed, may skip some tests. - "tests": [], + "tests": ["freezegun", "pytest-snapshot"], # used for local engine, which is only needed for unit tests at present. "polars": ["polars >= 1.7.0"], "scikit-learn": ["scikit-learn>=1.2.2"], @@ -82,7 +82,6 @@ "pre-commit", "nox", "google-cloud-testutils", - "freezegun", ], } extras["all"] = list(sorted(frozenset(itertools.chain.from_iterable(extras.values())))) diff --git a/tests/unit/core/compile/sqlglot/conftest.py b/tests/unit/core/compile/sqlglot/conftest.py index c9087abe88..23456fc76c 100644 --- a/tests/unit/core/compile/sqlglot/conftest.py +++ b/tests/unit/core/compile/sqlglot/conftest.py @@ -26,4 +26,15 @@ def compiler_session(): @pytest.fixture(scope="module") def all_types_df() -> pd.DataFrame: # TODO: all types pandas dataframes - return pd.DataFrame({}) + # TODO: add tests for empty dataframes + df = pd.DataFrame( + { + "int1": pd.Series([1, 2, 3], dtype="Int64"), + "int2": pd.Series([-10, 20, 30], dtype="Int64"), + "bools": pd.Series([True, None, False], dtype="boolean"), + "strings": pd.Series(["b", "aa", "ccc"], dtype="string[pyarrow]"), + }, + ) + # add more complexity index. + df.index = df.index.astype("Int64") + return df diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql new file mode 100644 index 0000000000..5c22910422 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql @@ -0,0 +1,3 @@ +SELECT + * +FROM UNNEST(ARRAY>[(1, -10, TRUE, 'b', 0), (2, 20, CAST(NULL AS BOOLEAN), 'aa', 1), (3, 30, FALSE, 'ccc', 2)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/test_compile_readlocal.py b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py index 2a69bb69b4..ad7cc5785a 100644 --- a/tests/unit/core/compile/sqlglot/test_compile_readlocal.py +++ b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py @@ -13,12 +13,16 @@ # limitations under the License. import pandas as pd +import pytest import bigframes import bigframes.pandas as bpd +pytest.importorskip("pytest_snapshot") -def test_compile_local(all_types_df: pd.DataFrame, compiler_session: bigframes.Session): + +def test_compile_readlocal( + all_types_df: pd.DataFrame, compiler_session: bigframes.Session, snapshot +): bf_df = bpd.DataFrame(all_types_df, session=compiler_session) - sql = bf_df.sql - assert sql == "SELECT" + snapshot.assert_match(bf_df.sql, "out.sql") From c958dbea32b77cec9fddfc09e3b40d1da220a42c Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 29 Apr 2025 10:27:51 -0700 Subject: [PATCH 21/28] perf: Fold row count ops when known (#1656) --- bigframes/core/array_value.py | 12 +++- bigframes/core/blocks.py | 28 +++------ bigframes/core/compile/compiler.py | 9 +-- bigframes/core/compile/polars/compiler.py | 5 -- bigframes/core/nodes.py | 62 ++++--------------- bigframes/core/rewrite/__init__.py | 2 + bigframes/core/rewrite/fold_row_count.py | 44 ++++++++++++++ bigframes/core/rewrite/order.py | 5 -- bigframes/core/rewrite/pruning.py | 24 ++++++-- bigframes/session/bq_caching_executor.py | 72 ++++++++--------------- bigframes/session/executor.py | 29 +++++++-- bigframes/session/local_scan_executor.py | 4 +- bigframes/session/read_api_execution.py | 10 +++- tests/system/small/test_session.py | 1 + tests/unit/core/test_blocks.py | 2 - 15 files changed, 160 insertions(+), 149 deletions(-) create mode 100644 bigframes/core/rewrite/fold_row_count.py diff --git a/bigframes/core/array_value.py b/bigframes/core/array_value.py index eba63ad72e..41a6882cd7 100644 --- a/bigframes/core/array_value.py +++ b/bigframes/core/array_value.py @@ -204,7 +204,17 @@ def get_column_type(self, key: str) -> bigframes.dtypes.Dtype: def row_count(self) -> ArrayValue: """Get number of rows in ArrayValue as a single-entry ArrayValue.""" - return ArrayValue(nodes.RowCountNode(child=self.node)) + return ArrayValue( + nodes.AggregateNode( + child=self.node, + aggregations=( + ( + ex.NullaryAggregation(agg_ops.size_op), + ids.ColumnId(bigframes.core.guid.generate_guid()), + ), + ), + ) + ) # Operations def filter_by_id(self, predicate_id: str, keep_null: bool = False) -> ArrayValue: diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index c53f392417..cc3b70f8a8 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -39,7 +39,6 @@ Optional, Sequence, Tuple, - TYPE_CHECKING, Union, ) import warnings @@ -68,13 +67,8 @@ import bigframes.core.window_spec as windows import bigframes.dtypes import bigframes.exceptions as bfe -import bigframes.features import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops -import bigframes.session._io.pandas as io_pandas - -if TYPE_CHECKING: - import bigframes.session.executor # Type constraint for wherever column labels are used Label = typing.Hashable @@ -221,7 +215,7 @@ def shape(self) -> typing.Tuple[int, int]: except Exception: pass - row_count = self.session._executor.get_row_count(self.expr) + row_count = self.session._executor.execute(self.expr.row_count()).to_py_scalar() return (row_count, len(self.value_columns)) @property @@ -485,7 +479,7 @@ def to_arrow( *, ordered: bool = True, allow_large_results: Optional[bool] = None, - ) -> Tuple[pa.Table, bigquery.QueryJob]: + ) -> Tuple[pa.Table, Optional[bigquery.QueryJob]]: """Run query and download results as a pyarrow Table.""" execute_result = self.session._executor.execute( self.expr, ordered=ordered, use_explicit_destination=allow_large_results @@ -580,7 +574,7 @@ def try_peek( result = self.session._executor.peek( self.expr, n, use_explicit_destination=allow_large_results ) - df = io_pandas.arrow_to_pandas(result.to_arrow_table(), self.expr.schema) + df = result.to_pandas() self._copy_index_to_pandas(df) return df else: @@ -604,8 +598,7 @@ def to_pandas_batches( page_size=page_size, max_results=max_results, ) - for record_batch in execute_result.arrow_batches(): - df = io_pandas.arrow_to_pandas(record_batch, self.expr.schema) + for df in execute_result.to_pandas_batches(): self._copy_index_to_pandas(df) if squeeze: yield df.squeeze(axis=1) @@ -659,7 +652,7 @@ def _materialize_local( # TODO: Maybe materialize before downsampling # Some downsampling methods - if fraction < 1: + if fraction < 1 and (execute_result.total_rows is not None): if not sample_config.enable_downsampling: raise RuntimeError( f"The data size ({table_mb:.2f} MB) exceeds the maximum download limit of " @@ -690,9 +683,7 @@ def _materialize_local( MaterializationOptions(ordered=materialize_options.ordered) ) else: - total_rows = execute_result.total_rows - arrow = execute_result.to_arrow_table() - df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) + df = execute_result.to_pandas() self._copy_index_to_pandas(df) return df, execute_result.query_job @@ -1570,12 +1561,11 @@ def retrieve_repr_request_results( # head caches full underlying expression, so row_count will be free after head_result = self.session._executor.head(self.expr, max_results) - count = self.session._executor.get_row_count(self.expr) + row_count = self.session._executor.execute(self.expr.row_count()).to_py_scalar() - arrow = head_result.to_arrow_table() - df = io_pandas.arrow_to_pandas(arrow, schema=self.expr.schema) + df = head_result.to_pandas() self._copy_index_to_pandas(df) - return df, count, head_result.query_job + return df, row_count, head_result.query_job def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]: expr, result_id = self._expr.promote_offsets() diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/compiler.py index 21045ca348..fb5399b7cb 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/compiler.py @@ -169,9 +169,7 @@ def compile_readlocal(node: nodes.ReadLocalNode, *args): bq_schema = node.schema.to_bigquery() pa_table = pa_table.select([item.source_id for item in node.scan_list.items]) - pa_table = pa_table.rename_columns( - {item.source_id: item.id.sql for item in node.scan_list.items} - ) + pa_table = pa_table.rename_columns([item.id.sql for item in node.scan_list.items]) if offsets: pa_table = pa_table.append_column( @@ -254,11 +252,6 @@ def compile_concat(node: nodes.ConcatNode, *children: compiled.UnorderedIR): return concat_impl.concat_unordered(children, output_ids) -@_compile_node.register -def compile_rowcount(node: nodes.RowCountNode, child: compiled.UnorderedIR): - return child.row_count(name=node.col_id.sql) - - @_compile_node.register def compile_aggregate(node: nodes.AggregateNode, child: compiled.UnorderedIR): aggs = tuple((agg, id.sql) for agg, id in node.aggregations) diff --git a/bigframes/core/compile/polars/compiler.py b/bigframes/core/compile/polars/compiler.py index baa19eb990..b2f018e80a 100644 --- a/bigframes/core/compile/polars/compiler.py +++ b/bigframes/core/compile/polars/compiler.py @@ -252,11 +252,6 @@ def compile_projection(self, node: nodes.ProjectionNode): ] return self.compile_node(node.child).with_columns(new_cols) - @compile_node.register - def compile_rowcount(self, node: nodes.RowCountNode): - df = cast(pl.LazyFrame, self.compile_node(node.child)) - return df.select(pl.len().alias(node.col_id.sql)) - @compile_node.register def compile_offsets(self, node: nodes.PromoteOffsetsNode): return self.compile_node(node.child).with_columns( diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 558e4a2aaf..0fbfe7bd37 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -1256,55 +1256,6 @@ def remap_refs( return dataclasses.replace(self, assignments=new_fields) -# TODO: Merge RowCount into Aggregate Node? -# Row count can be compute from table metadata sometimes, so it is a bit special. -@dataclasses.dataclass(frozen=True, eq=False) -class RowCountNode(UnaryNode): - col_id: identifiers.ColumnId = identifiers.ColumnId("count") - - @property - def row_preserving(self) -> bool: - return False - - @property - def non_local(self) -> bool: - return True - - @property - def fields(self) -> Sequence[Field]: - return (Field(self.col_id, bigframes.dtypes.INT_DTYPE, nullable=False),) - - @property - def variables_introduced(self) -> int: - return 1 - - @property - def defines_namespace(self) -> bool: - return True - - @property - def row_count(self) -> Optional[int]: - return 1 - - @property - def node_defined_ids(self) -> Tuple[identifiers.ColumnId, ...]: - return (self.col_id,) - - @property - def consumed_ids(self) -> COLUMN_SET: - return frozenset() - - def remap_vars( - self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] - ) -> RowCountNode: - return dataclasses.replace(self, col_id=mappings.get(self.col_id, self.col_id)) - - def remap_refs( - self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] - ) -> RowCountNode: - return self - - @dataclasses.dataclass(frozen=True, eq=False) class AggregateNode(UnaryNode): aggregations: typing.Tuple[typing.Tuple[ex.Aggregation, identifiers.ColumnId], ...] @@ -1642,6 +1593,19 @@ def remap_refs( order_by = self.order_by.remap_column_refs(mappings) if self.order_by else None return dataclasses.replace(self, output_cols=output_cols, order_by=order_by) # type: ignore + @property + def fields(self) -> Sequence[Field]: + # Fields property here is for output schema, not to be consumed by a parent node. + input_fields_by_id = {field.id: field for field in self.child.fields} + return tuple( + Field( + identifiers.ColumnId(output), + input_fields_by_id[ref.id].dtype, + input_fields_by_id[ref.id].nullable, + ) + for ref, output in self.output_cols + ) + @property def consumed_ids(self) -> COLUMN_SET: out_refs = frozenset(ref.id for ref, _ in self.output_cols) diff --git a/bigframes/core/rewrite/__init__.py b/bigframes/core/rewrite/__init__.py index e9a94069f5..069ebb9cdf 100644 --- a/bigframes/core/rewrite/__init__.py +++ b/bigframes/core/rewrite/__init__.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from bigframes.core.rewrite.fold_row_count import fold_row_counts from bigframes.core.rewrite.identifiers import remap_variables from bigframes.core.rewrite.implicit_align import try_row_join from bigframes.core.rewrite.legacy_align import legacy_join_as_projection @@ -38,4 +39,5 @@ "try_reduce_to_table_scan", "bake_order", "try_reduce_to_local_scan", + "fold_row_counts", ] diff --git a/bigframes/core/rewrite/fold_row_count.py b/bigframes/core/rewrite/fold_row_count.py new file mode 100644 index 0000000000..583343d68a --- /dev/null +++ b/bigframes/core/rewrite/fold_row_count.py @@ -0,0 +1,44 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import pyarrow as pa + +from bigframes import dtypes +from bigframes.core import local_data, nodes +from bigframes.operations import aggregations + + +def fold_row_counts(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + if not isinstance(node, nodes.AggregateNode): + return node + if len(node.by_column_ids) > 0: + return node + if node.child.row_count is None: + return node + for agg, _ in node.aggregations: + if agg.op != aggregations.size_op: + return node + local_data_source = local_data.ManagedArrowTable.from_pyarrow( + pa.table({"count": pa.array([node.child.row_count], type=pa.int64())}) + ) + scan_list = nodes.ScanList( + tuple( + nodes.ScanItem(out_id, dtypes.INT_DTYPE, "count") + for _, out_id in node.aggregations + ) + ) + return nodes.ReadLocalNode( + local_data_source=local_data_source, scan_list=scan_list, session=node.session + ) diff --git a/bigframes/core/rewrite/order.py b/bigframes/core/rewrite/order.py index 06deba996c..5b5fb10753 100644 --- a/bigframes/core/rewrite/order.py +++ b/bigframes/core/rewrite/order.py @@ -211,11 +211,6 @@ def pull_up_order_inner( ) new_order = child_order.remap_column_refs(new_select_node.get_id_mapping()) return new_select_node, new_order - elif isinstance(node, bigframes.core.nodes.RowCountNode): - child_result = remove_order(node.child) - return node.replace_child( - child_result - ), bigframes.core.ordering.TotalOrdering.from_primary_key([node.col_id]) elif isinstance(node, bigframes.core.nodes.AggregateNode): if node.has_ordered_ops: child_result, child_order = pull_up_order_inner(node.child) diff --git a/bigframes/core/rewrite/pruning.py b/bigframes/core/rewrite/pruning.py index 61375cf352..1ecfb452ec 100644 --- a/bigframes/core/rewrite/pruning.py +++ b/bigframes/core/rewrite/pruning.py @@ -51,9 +51,17 @@ def prune_columns(node: nodes.BigFrameNode): if isinstance(node, nodes.SelectionNode): result = prune_selection_child(node) elif isinstance(node, nodes.ResultNode): - result = node.replace_child(prune_node(node.child, node.consumed_ids)) + result = node.replace_child( + prune_node( + node.child, node.consumed_ids or frozenset(list(node.child.ids)[0:1]) + ) + ) elif isinstance(node, nodes.AggregateNode): - result = node.replace_child(prune_node(node.child, node.consumed_ids)) + result = node.replace_child( + prune_node( + node.child, node.consumed_ids or frozenset(list(node.child.ids)[0:1]) + ) + ) elif isinstance(node, nodes.InNode): result = dataclasses.replace( node, @@ -71,7 +79,9 @@ def prune_selection_child( # Important to check this first if list(selection.ids) == list(child.ids): - return child + if (ref.ref.id == ref.id for ref in selection.input_output_pairs): + # selection is no-op so just remove it entirely + return child if isinstance(child, nodes.SelectionNode): return selection.remap_refs( @@ -96,6 +106,9 @@ def prune_selection_child( indices = [ list(child.ids).index(ref.id) for ref, _ in selection.input_output_pairs ] + if len(indices) == 0: + # pushing zero-column selection into concat messes up emitter for now, which doesn't like zero columns + return selection new_children = [] for concat_node in child.child_nodes: cc_ids = tuple(concat_node.ids) @@ -146,7 +159,10 @@ def prune_aggregate( node: nodes.AggregateNode, used_cols: AbstractSet[identifiers.ColumnId], ) -> nodes.AggregateNode: - pruned_aggs = tuple(agg for agg in node.aggregations if agg[1] in used_cols) + pruned_aggs = ( + tuple(agg for agg in node.aggregations if agg[1] in used_cols) + or node.aggregations[0:1] + ) return dataclasses.replace(node, aggregations=pruned_aggs) diff --git a/bigframes/session/bq_caching_executor.py b/bigframes/session/bq_caching_executor.py index 584b41452a..ec5795f9a8 100644 --- a/bigframes/session/bq_caching_executor.py +++ b/bigframes/session/bq_caching_executor.py @@ -27,6 +27,7 @@ import google.cloud.bigquery_storage_v1 import bigframes.core +from bigframes.core import rewrite import bigframes.core.compile import bigframes.core.guid import bigframes.core.nodes as nodes @@ -96,11 +97,7 @@ def to_sql( ) -> str: if offset_column: array_value, _ = array_value.promote_offsets() - node = ( - self.replace_cached_subtrees(array_value.node) - if enable_cache - else array_value.node - ) + node = self.logical_plan(array_value.node) if enable_cache else array_value.node return self.compiler.compile(node, ordered=ordered) def execute( @@ -118,7 +115,7 @@ def execute( if bigframes.options.compute.enable_multi_query_execution: self._simplify_with_caching(array_value) - plan = self.replace_cached_subtrees(array_value.node) + plan = self.logical_plan(array_value.node) # Use explicit destination to avoid 10GB limit of temporary table destination_table = ( self.storage_manager.create_temp_table( @@ -227,7 +224,7 @@ def peek( """ A 'peek' efficiently accesses a small number of rows in the dataframe. """ - plan = self.replace_cached_subtrees(array_value.node) + plan = self.logical_plan(array_value.node) if not tree_properties.can_fast_peek(plan): msg = bfe.format_message("Peeking this value cannot be done efficiently.") warnings.warn(msg) @@ -249,16 +246,14 @@ def peek( def head( self, array_value: bigframes.core.ArrayValue, n_rows: int ) -> executor.ExecuteResult: - - maybe_row_count = self._local_get_row_count(array_value) - if (maybe_row_count is not None) and (maybe_row_count <= n_rows): - return self.execute(array_value, ordered=True) + plan = self.logical_plan(array_value.node) + if (plan.row_count is not None) and (plan.row_count <= n_rows): + return self._execute_plan(plan, ordered=True) if not self.strictly_ordered and not array_value.node.explicitly_ordered: # No user-provided ordering, so just get any N rows, its faster! return self.peek(array_value, n_rows) - plan = self.replace_cached_subtrees(array_value.node) if not tree_properties.can_fast_head(plan): # If can't get head fast, we are going to need to execute the whole query # Will want to do this in a way such that the result is reusable, but the first @@ -266,26 +261,12 @@ def head( # This currently requires clustering on offsets. self._cache_with_offsets(array_value) # Get a new optimized plan after caching - plan = self.replace_cached_subtrees(array_value.node) + plan = self.logical_plan(array_value.node) assert tree_properties.can_fast_head(plan) head_plan = generate_head_plan(plan, n_rows) return self._execute_plan(head_plan, ordered=True) - def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: - # TODO: Fold row count node in and use local execution - count = self._local_get_row_count(array_value) - if count is not None: - return count - else: - row_count_plan = self.replace_cached_subtrees( - generate_row_count_plan(array_value.node) - ) - results = self._execute_plan(row_count_plan, ordered=True) - pa_table = next(results.arrow_batches()) - pa_array = pa_table.column(0) - return pa_array.tolist()[0] - def cached( self, array_value: bigframes.core.ArrayValue, @@ -303,14 +284,6 @@ def cached( else: self._cache_with_cluster_cols(array_value, cluster_cols=cluster_cols) - def _local_get_row_count( - self, array_value: bigframes.core.ArrayValue - ) -> Optional[int]: - # optimized plan has cache materializations which will have row count metadata - # that is more likely to be usable than original leaf nodes. - plan = self.replace_cached_subtrees(array_value.node) - return tree_properties.row_count(plan) - # Helpers def _run_execute_query( self, @@ -365,16 +338,25 @@ def _is_trivially_executable(self, array_value: bigframes.core.ArrayValue): # Once rewriting is available, will want to rewrite before # evaluating execution cost. return tree_properties.is_trivially_executable( - self.replace_cached_subtrees(array_value.node) + self.logical_plan(array_value.node) ) + def logical_plan(self, root: nodes.BigFrameNode) -> nodes.BigFrameNode: + """ + Apply universal logical simplifications that are helpful regardless of engine. + """ + plan = self.replace_cached_subtrees(root) + plan = rewrite.column_pruning(plan) + plan = plan.top_down(rewrite.fold_row_counts) + return plan + def _cache_with_cluster_cols( self, array_value: bigframes.core.ArrayValue, cluster_cols: Sequence[str] ): """Executes the query and uses the resulting table to rewrite future executions.""" sql, schema, ordering_info = self.compiler.compile_raw( - self.replace_cached_subtrees(array_value.node) + self.logical_plan(array_value.node) ) tmp_table = self._sql_as_cached_temp_table( sql, @@ -391,9 +373,7 @@ def _cache_with_offsets(self, array_value: bigframes.core.ArrayValue): """Executes the query and uses the resulting table to rewrite future executions.""" offset_column = bigframes.core.guid.generate_guid("bigframes_offsets") w_offsets, offset_column = array_value.promote_offsets() - sql = self.compiler.compile( - self.replace_cached_subtrees(w_offsets.node), ordered=False - ) + sql = self.compiler.compile(self.logical_plan(w_offsets.node), ordered=False) tmp_table = self._sql_as_cached_temp_table( sql, @@ -429,8 +409,10 @@ def _simplify_with_caching(self, array_value: bigframes.core.ArrayValue): """Attempts to handle the complexity by caching duplicated subtrees and breaking the query into pieces.""" # Apply existing caching first for _ in range(MAX_SUBTREE_FACTORINGS): - node_with_cache = self.replace_cached_subtrees(array_value.node) - if node_with_cache.planning_complexity < QUERY_COMPLEXITY_LIMIT: + if ( + self.logical_plan(array_value.node).planning_complexity + < QUERY_COMPLEXITY_LIMIT + ): return did_cache = self._cache_most_complex_subtree(array_value.node) @@ -486,7 +468,7 @@ def _validate_result_schema( ): actual_schema = _sanitize(tuple(bq_schema)) ibis_schema = bigframes.core.compile.test_only_ibis_inferred_schema( - self.replace_cached_subtrees(array_value.node) + self.logical_plan(array_value.node) ).to_bigquery() internal_schema = _sanitize(array_value.schema.to_bigquery()) if not bigframes.features.PANDAS_VERSIONS.is_arrow_list_dtype_usable: @@ -593,7 +575,3 @@ def _sanitize( def generate_head_plan(node: nodes.BigFrameNode, n: int): return nodes.SliceNode(node, start=None, stop=n) - - -def generate_row_count_plan(node: nodes.BigFrameNode): - return nodes.RowCountNode(node) diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 496ff560dd..0ba4ee3c2d 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -16,14 +16,17 @@ import abc import dataclasses +import functools import itertools from typing import Callable, Iterator, Literal, Mapping, Optional, Sequence, Union from google.cloud import bigquery +import pandas as pd import pyarrow import bigframes.core import bigframes.core.schema +import bigframes.session._io.pandas as io_pandas @dataclasses.dataclass(frozen=True) @@ -49,6 +52,26 @@ def to_arrow_table(self) -> pyarrow.Table: else: return self.schema.to_pyarrow().empty_table() + def to_pandas(self) -> pd.DataFrame: + return io_pandas.arrow_to_pandas(self.to_arrow_table(), self.schema) + + def to_pandas_batches(self) -> Iterator[pd.DataFrame]: + yield from map( + functools.partial(io_pandas.arrow_to_pandas, schema=self.schema), + self.arrow_batches(), + ) + + def to_py_scalar(self): + columns = list(self.to_arrow_table().to_pydict().values()) + if len(columns) != 1: + raise ValueError( + f"Expected single column result, got {len(columns)} columns." + ) + column = columns[0] + if len(column) != 1: + raise ValueError(f"Expected single row result, got {len(column)} rows.") + return column[0] + class Executor(abc.ABC): """ @@ -75,7 +98,7 @@ def execute( use_explicit_destination: Optional[bool] = False, page_size: Optional[int] = None, max_results: Optional[int] = None, - ): + ) -> ExecuteResult: """ Execute the ArrayValue, storing the result to a temporary session-owned table. """ @@ -135,10 +158,6 @@ def head( """ raise NotImplementedError("head not implemented for this executor") - # TODO: This should be done through execute() - def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: - raise NotImplementedError("get_row_count not implemented for this executor") - def cached( self, array_value: bigframes.core.ArrayValue, diff --git a/bigframes/session/local_scan_executor.py b/bigframes/session/local_scan_executor.py index 3ed1b759c5..67e381ab8a 100644 --- a/bigframes/session/local_scan_executor.py +++ b/bigframes/session/local_scan_executor.py @@ -50,9 +50,7 @@ def iterator_supplier(): needed_cols.append(offsets_col) arrow_table = arrow_table.select(needed_cols) - arrow_table = arrow_table.rename_columns( - {item.source_id: item.id.sql for item in node.scan_list.items} - ) + arrow_table = arrow_table.rename_columns([id.sql for id in node.ids]) yield from arrow_table.to_batches() total_rows = node.row_count diff --git a/bigframes/session/read_api_execution.py b/bigframes/session/read_api_execution.py index 32095e41f4..ae1272e722 100644 --- a/bigframes/session/read_api_execution.py +++ b/bigframes/session/read_api_execution.py @@ -16,6 +16,7 @@ from typing import Any, Optional from google.cloud import bigquery_storage_v1 +import pyarrow as pa from bigframes.core import bigframe_node, rewrite from bigframes.session import executor, semi_executor @@ -89,7 +90,14 @@ def iterator_supplier(): session.streams[0].name, retry=None ) rowstream = reader.rows() - return map(lambda page: page.to_arrow(), rowstream.pages) + + def process_page(page): + pa_batch = page.to_arrow() + return pa.RecordBatch.from_arrays( + pa_batch.columns, names=[id.sql for id in node.ids] + ) + + return map(process_page, rowstream.pages) return executor.ExecuteResult( arrow_batches=iterator_supplier, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 1b886fbc5a..c7bf5b3f5e 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -871,6 +871,7 @@ def test_read_pandas_tokyo( result = session_tokyo._executor.execute( df._block.expr, use_explicit_destination=True ) + assert result.query_job is not None assert result.query_job.location == tokyo_location assert len(expected) == result.total_rows diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index b1b276bda3..7c06bedfd3 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -85,13 +85,11 @@ def test_block_from_local(data): # hard-coded the returned dimension of the session for that each of the test case contains 3 rows. mock_session._executor = mock_executor - mock_executor.get_row_count.return_value = 3 block = blocks.Block.from_local(pandas.DataFrame(data), mock_session) pandas.testing.assert_index_equal(block.column_labels, expected.columns) assert tuple(block.index.names) == tuple(expected.index.names) - assert block.shape == expected.shape def test_block_compute_dry_run__raises_error_when_sampling_is_enabled(): From 4fb54dfe448604a90fc1818cf18b1e77e1e7227b Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 29 Apr 2025 10:37:08 -0700 Subject: [PATCH 22/28] feat: add gemini-2.0-flash-001 and gemini-2.0-flash-lite-001 to fine tune score endponts and multimodal endpoints (#1650) * add test, no code support yet * add gemini-2.0-xx to fine tune score endponts and multimodal endpoints * wait for bqml to support gemini-2.0-flash-lite-001 * remove unsupported GA feature * remove unsupported fine-tune endpoints * fix a failed test * remove features are not ready * fix failed test * revert a typo --- bigframes/ml/llm.py | 8 ++++++-- tests/system/small/ml/test_llm.py | 10 ++++++++++ tests/system/small/ml/test_multimodal_llm.py | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 49dbfd3443..3aecc34142 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -72,6 +72,8 @@ _GEMINI_FINE_TUNE_SCORE_ENDPOINTS = ( _GEMINI_1P5_PRO_002_ENDPOINT, _GEMINI_1P5_FLASH_002_ENDPOINT, + _GEMINI_2_FLASH_001_ENDPOINT, + _GEMINI_2_FLASH_LITE_001_ENDPOINT, ) _GEMINI_MULTIMODAL_ENDPOINTS = ( _GEMINI_1P5_PRO_001_ENDPOINT, @@ -79,6 +81,7 @@ _GEMINI_1P5_FLASH_001_ENDPOINT, _GEMINI_1P5_FLASH_002_ENDPOINT, _GEMINI_2_FLASH_EXP_ENDPOINT, + _GEMINI_2_FLASH_001_ENDPOINT, ) _CLAUDE_3_SONNET_ENDPOINT = "claude-3-sonnet" @@ -712,7 +715,8 @@ def score( ] = "text_generation", ) -> bigframes.dataframe.DataFrame: """Calculate evaluation metrics of the model. Only support - "gemini-1.5-pro-002", and "gemini-1.5-flash-002". + "gemini-1.5-pro-002", "gemini-1.5-flash-002", + "gemini-2.0-flash-lite-001", and "gemini-2.0-flash-001". .. note:: @@ -746,7 +750,7 @@ def score( if self.model_name not in _GEMINI_FINE_TUNE_SCORE_ENDPOINTS: raise NotImplementedError( - "score() only supports gemini-1.5-pro-002, and gemini-1.5-flash-2 model." + "score() only supports gemini-1.5-pro-002, gemini-1.5-flash-2, gemini-2.0-flash-001, and gemini-2.0-flash-lite-001 model." ) X, y = utils.batch_convert_to_dataframe(X, y, session=self._bqml_model.session) diff --git a/tests/system/small/ml/test_llm.py b/tests/system/small/ml/test_llm.py index 90483667d8..51e9d8ad6a 100644 --- a/tests/system/small/ml/test_llm.py +++ b/tests/system/small/ml/test_llm.py @@ -152,6 +152,8 @@ def test_create_load_gemini_text_generator_model( "gemini-1.5-flash-001", "gemini-1.5-flash-002", "gemini-2.0-flash-exp", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", ), ) @pytest.mark.flaky(retries=2) @@ -177,6 +179,8 @@ def test_gemini_text_generator_predict_default_params_success( "gemini-1.5-flash-001", "gemini-1.5-flash-002", "gemini-2.0-flash-exp", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", ), ) @pytest.mark.flaky(retries=2) @@ -204,6 +208,8 @@ def test_gemini_text_generator_predict_with_params_success( "gemini-1.5-flash-001", "gemini-1.5-flash-002", "gemini-2.0-flash-exp", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", ), ) @pytest.mark.flaky(retries=2) @@ -764,6 +770,8 @@ def test_text_embedding_generator_retry_no_progress(session, bq_connection): ( "gemini-1.5-pro-002", "gemini-1.5-flash-002", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", ), ) def test_llm_gemini_score(llm_fine_tune_df_default_index, model_name): @@ -792,6 +800,8 @@ def test_llm_gemini_score(llm_fine_tune_df_default_index, model_name): ( "gemini-1.5-pro-002", "gemini-1.5-flash-002", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", ), ) def test_llm_gemini_pro_score_params(llm_fine_tune_df_default_index, model_name): diff --git a/tests/system/small/ml/test_multimodal_llm.py b/tests/system/small/ml/test_multimodal_llm.py index 51e6bcb2d5..7c07d9ead2 100644 --- a/tests/system/small/ml/test_multimodal_llm.py +++ b/tests/system/small/ml/test_multimodal_llm.py @@ -47,6 +47,7 @@ def test_multimodal_embedding_generator_predict_default_params_success( "gemini-1.5-flash-001", "gemini-1.5-flash-002", "gemini-2.0-flash-exp", + "gemini-2.0-flash-001", ), ) @pytest.mark.flaky(retries=2) From 563f0cbdf4a18c3cd1bd2a4b52de823165638911 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:26:26 -0700 Subject: [PATCH 23/28] feat: DataFrames.__getitem__ support for slice input (#1668) --- bigframes/dataframe.py | 3 +++ tests/system/small/test_dataframe.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 101e7a1c5e..9cb388329e 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -576,6 +576,9 @@ def __getitem__( if isinstance(key, bigframes.series.Series): return self._getitem_bool_series(key) + if isinstance(key, slice): + return self.iloc[key] + if isinstance(key, typing.Hashable): return self._getitem_label(key) # Select a subset of columns or re-order columns. diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 452fa841e4..362d736aeb 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -221,6 +221,21 @@ def test_get_column_nonstring(scalars_dfs): assert_series_equal(bf_result, pd_result) +@pytest.mark.parametrize( + "row_slice", + [ + (slice(1, 7, 2)), + (slice(1, 7, None)), + (slice(None, -3, None)), + ], +) +def test_get_rows_with_slice(scalars_dfs, row_slice): + scalars_df, scalars_pandas_df = scalars_dfs + bf_result = scalars_df[row_slice].to_pandas() + pd_result = scalars_pandas_df[row_slice] + assert_pandas_df_equal(bf_result, pd_result) + + def test_hasattr(scalars_dfs): scalars_df, _ = scalars_dfs assert hasattr(scalars_df, "int64_col") From ae312dbed25da6da5e2817d5c9838654c2a1ad1c Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:15:11 -0700 Subject: [PATCH 24/28] =?UTF-8?q?feat:=20session.bytes=5Fprocessed=5Fsum?= =?UTF-8?q?=20will=20be=20updated=20when=20allow=5Flarge=5Fre=E2=80=A6=20(?= =?UTF-8?q?#1669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: session.bytes_processed_sum will be updated when allow_large_result=False * mypy fix * remove duplicate logic --- bigframes/session/__init__.py | 7 --- bigframes/session/_io/bigquery/__init__.py | 5 +-- bigframes/session/metrics.py | 51 +++++++++++----------- scripts/run_and_publish_benchmark.py | 43 ++++++++---------- 4 files changed, 45 insertions(+), 61 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 6379a6f2e8..c38d124196 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -336,13 +336,6 @@ def _project(self): @property def bytes_processed_sum(self): """The sum of all bytes processed by bigquery jobs using this session.""" - msg = bfe.format_message( - "Queries executed with `allow_large_results=False` within the session will not " - "have their bytes processed counted in this sum. If you need precise " - "bytes processed information, query the `INFORMATION_SCHEMA` tables " - "to get relevant metrics.", - ) - warnings.warn(msg, UserWarning) return self._metrics.bytes_processed @property diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 4fdd836777..6df9424e3b 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -27,7 +27,6 @@ import bigframes_vendored.pandas.io.gbq as third_party_pandas_gbq import google.api_core.exceptions import google.cloud.bigquery as bigquery -import google.cloud.bigquery.table from bigframes.core import log_adapter import bigframes.core.compile.googlesql as googlesql @@ -249,7 +248,7 @@ def start_query_with_client( max_results=max_results, ) if metrics is not None: - metrics.count_job_stats(query=sql) + metrics.count_job_stats(row_iterator=results_iterator) return results_iterator, None query_job = bq_client.query( @@ -278,7 +277,7 @@ def start_query_with_client( ) if metrics is not None: - metrics.count_job_stats(query_job) + metrics.count_job_stats(query_job=query_job) return results_iterator, query_job diff --git a/bigframes/session/metrics.py b/bigframes/session/metrics.py index b4e1458b21..055abd5899 100644 --- a/bigframes/session/metrics.py +++ b/bigframes/session/metrics.py @@ -20,6 +20,7 @@ import google.cloud.bigquery as bigquery import google.cloud.bigquery.job as bq_job +import google.cloud.bigquery.table as bq_table LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" @@ -33,14 +34,22 @@ class ExecutionMetrics: query_char_count: int = 0 def count_job_stats( - self, query_job: Optional[bq_job.QueryJob] = None, query: str = "" + self, + query_job: Optional[bq_job.QueryJob] = None, + row_iterator: Optional[bq_table.RowIterator] = None, ): if query_job is None: - query_char_count = len(query) + assert row_iterator is not None + if (row_iterator.total_bytes_processed is None) or ( + row_iterator.query is None + ): + return + query_char_count = len(row_iterator.query) + bytes_processed = row_iterator.total_bytes_processed self.execution_count += 1 self.query_char_count += query_char_count - if LOGGING_NAME_ENV_VAR in os.environ: - write_stats_to_disk(query_char_count) + self.bytes_processed += bytes_processed + write_stats_to_disk(query_char_count, bytes_processed) return stats = get_performance_stats(query_job) @@ -51,11 +60,9 @@ def count_job_stats( self.bytes_processed += bytes_processed self.slot_millis += slot_millis self.execution_secs += execution_secs - if LOGGING_NAME_ENV_VAR in os.environ: - # when running notebooks via pytest nbmake - write_stats_to_disk( - query_char_count, bytes_processed, slot_millis, execution_secs - ) + write_stats_to_disk( + query_char_count, bytes_processed, slot_millis, execution_secs + ) def get_performance_stats( @@ -88,7 +95,7 @@ def get_performance_stats( def write_stats_to_disk( query_char_count: int, - bytes_processed: Optional[int] = None, + bytes_processed: int, slot_millis: Optional[int] = None, exec_seconds: Optional[float] = None, ): @@ -96,24 +103,13 @@ def write_stats_to_disk( to a file in order to create a performance report. """ if LOGGING_NAME_ENV_VAR not in os.environ: - raise EnvironmentError( - "Environment variable {env_var} is not set".format( - env_var=LOGGING_NAME_ENV_VAR - ) - ) + return + + # when running notebooks via pytest nbmake and running benchmarks test_name = os.environ[LOGGING_NAME_ENV_VAR] current_directory = os.getcwd() - if ( - (bytes_processed is not None) - and (slot_millis is not None) - and (exec_seconds is not None) - ): - # store bytes processed - bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") - with open(bytes_file, "a") as f: - f.write(str(bytes_processed) + "\n") - + if (slot_millis is not None) and (exec_seconds is not None): # store slot milliseconds slot_file = os.path.join(current_directory, test_name + ".slotmillis") with open(slot_file, "a") as f: @@ -132,3 +128,8 @@ def write_stats_to_disk( ) with open(query_char_count_file, "a") as f: f.write(str(query_char_count) + "\n") + + # store bytes processed + bytes_file = os.path.join(current_directory, test_name + ".bytesprocessed") + with open(bytes_file, "a") as f: + f.write(str(bytes_processed) + "\n") diff --git a/scripts/run_and_publish_benchmark.py b/scripts/run_and_publish_benchmark.py index 402ba4d213..0ea3a5e162 100644 --- a/scripts/run_and_publish_benchmark.py +++ b/scripts/run_and_publish_benchmark.py @@ -93,10 +93,10 @@ def collect_benchmark_result( error_files = sorted(path.rglob("*.error")) if not ( - len(bytes_files) - == len(millis_files) + len(millis_files) == len(bq_seconds_files) - <= len(query_char_count_files) + <= len(bytes_files) + == len(query_char_count_files) == len(local_seconds_files) ): raise ValueError( @@ -108,10 +108,13 @@ def collect_benchmark_result( for idx in range(len(local_seconds_files)): query_char_count_file = query_char_count_files[idx] local_seconds_file = local_seconds_files[idx] + bytes_file = bytes_files[idx] filename = query_char_count_file.relative_to(path).with_suffix("") - if filename != local_seconds_file.relative_to(path).with_suffix(""): + if filename != local_seconds_file.relative_to(path).with_suffix( + "" + ) or filename != bytes_file.relative_to(path).with_suffix(""): raise ValueError( - "File name mismatch between query_char_count and seconds reports." + "File name mismatch among query_char_count, bytes and seconds reports." ) with open(query_char_count_file, "r") as file: @@ -123,27 +126,23 @@ def collect_benchmark_result( lines = file.read().splitlines() local_seconds = sum(float(line) for line in lines) / iterations + with open(bytes_file, "r") as file: + lines = file.read().splitlines() + total_bytes = sum(int(line) for line in lines) / iterations + if not has_full_metrics: - total_bytes = None total_slot_millis = None bq_seconds = None else: - bytes_file = bytes_files[idx] millis_file = millis_files[idx] bq_seconds_file = bq_seconds_files[idx] - if ( - filename != bytes_file.relative_to(path).with_suffix("") - or filename != millis_file.relative_to(path).with_suffix("") - or filename != bq_seconds_file.relative_to(path).with_suffix("") - ): + if filename != millis_file.relative_to(path).with_suffix( + "" + ) or filename != bq_seconds_file.relative_to(path).with_suffix(""): raise ValueError( "File name mismatch among query_char_count, bytes, millis, and seconds reports." ) - with open(bytes_file, "r") as file: - lines = file.read().splitlines() - total_bytes = sum(int(line) for line in lines) / iterations - with open(millis_file, "r") as file: lines = file.read().splitlines() total_slot_millis = sum(int(line) for line in lines) / iterations @@ -202,11 +201,7 @@ def collect_benchmark_result( print( f"{index} - query count: {row['Query_Count']}," + f" query char count: {row['Query_Char_Count']}," - + ( - f" bytes processed sum: {row['Bytes_Processed']}," - if has_full_metrics - else "" - ) + + f" bytes processed sum: {row['Bytes_Processed']}," + (f" slot millis sum: {row['Slot_Millis']}," if has_full_metrics else "") + f" local execution time: {formatted_local_exec_time} seconds" + ( @@ -238,11 +233,7 @@ def collect_benchmark_result( print( f"---Geometric mean of queries: {geometric_mean_queries}," + f" Geometric mean of queries char counts: {geometric_mean_query_char_count}," - + ( - f" Geometric mean of bytes processed: {geometric_mean_bytes}," - if has_full_metrics - else "" - ) + + f" Geometric mean of bytes processed: {geometric_mean_bytes}," + ( f" Geometric mean of slot millis: {geometric_mean_slot_millis}," if has_full_metrics From 33881914ab5b8d0e701eabd9c731aed1deab3d49 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Tue, 29 Apr 2025 18:11:41 -0700 Subject: [PATCH 25/28] feat: support names parameter in read_csv for bigquery engine (#1659) --- bigframes/core/utils.py | 6 +- bigframes/session/__init__.py | 36 +++--- .../session/_io/bigquery/read_gbq_table.py | 14 +++ bigframes/session/loader.py | 64 +++++++---- tests/system/small/test_session.py | 108 +++++++++++++++++- tests/unit/session/test_session.py | 24 +++- .../pandas/io/parsers/readers.py | 2 +- 7 files changed, 205 insertions(+), 49 deletions(-) diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index 9731857ea0..495523d2fc 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -41,8 +41,10 @@ def get_axis_number(axis: typing.Union[str, int]) -> typing.Literal[0, 1]: raise ValueError(f"Not a valid axis: {axis}") -def is_list_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Sequence]: - return pd.api.types.is_list_like(obj) +def is_list_like( + obj: typing.Any, allow_sets: bool = True +) -> typing_extensions.TypeGuard[typing.Sequence]: + return pd.api.types.is_list_like(obj, allow_sets=allow_sets) def is_dict_like(obj: typing.Any) -> typing_extensions.TypeGuard[typing.Mapping]: diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index c38d124196..6801937fbe 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -16,6 +16,7 @@ from __future__ import annotations +from collections import abc import datetime import logging import os @@ -569,7 +570,7 @@ def read_gbq_table( columns = col_order return self._loader.read_gbq_table( - query=query, + table_id=query, index_col=index_col, columns=columns, max_results=max_results, @@ -953,14 +954,21 @@ def _read_csv_w_bigquery_engine( native CSV loading capabilities, making it suitable for large datasets that may not fit into local memory. """ - - if any(param is not None for param in (dtype, names)): - not_supported = ("dtype", "names") + if dtype is not None: raise NotImplementedError( - f"BigQuery engine does not support these arguments: {not_supported}. " + f"BigQuery engine does not support the `dtype` argument." f"{constants.FEEDBACK_LINK}" ) + if names is not None: + if len(names) != len(set(names)): + raise ValueError("Duplicated names are not allowed.") + if not ( + bigframes.core.utils.is_list_like(names, allow_sets=False) + or isinstance(names, abc.KeysView) + ): + raise ValueError("Names should be an ordered collection.") + if index_col is True: raise ValueError("The value of index_col couldn't be 'True'") @@ -1004,11 +1012,9 @@ def _read_csv_w_bigquery_engine( elif header > 0: job_config.skip_leading_rows = header + 1 - return self._loader.read_bigquery_load_job( - filepath_or_buffer, - job_config=job_config, - index_col=index_col, - columns=columns, + table_id = self._loader.load_file(filepath_or_buffer, job_config=job_config) + return self._loader.read_gbq_table( + table_id, index_col=index_col, columns=columns, names=names ) def read_pickle( @@ -1049,8 +1055,8 @@ def read_parquet( job_config = bigquery.LoadJobConfig() job_config.source_format = bigquery.SourceFormat.PARQUET job_config.labels = {"bigframes-api": "read_parquet"} - - return self._loader.read_bigquery_load_job(path, job_config=job_config) + table_id = self._loader.load_file(path, job_config=job_config) + return self._loader.read_gbq_table(table_id) else: if "*" in path: raise ValueError( @@ -1121,10 +1127,8 @@ def read_json( job_config.encoding = encoding job_config.labels = {"bigframes-api": "read_json"} - return self._loader.read_bigquery_load_job( - path_or_buf, - job_config=job_config, - ) + table_id = self._loader.load_file(path_or_buf, job_config=job_config) + return self._loader.read_gbq_table(table_id) else: if any(arg in kwargs for arg in ("chunksize", "iterator")): raise NotImplementedError( diff --git a/bigframes/session/_io/bigquery/read_gbq_table.py b/bigframes/session/_io/bigquery/read_gbq_table.py index 34183b22bc..8d8f247185 100644 --- a/bigframes/session/_io/bigquery/read_gbq_table.py +++ b/bigframes/session/_io/bigquery/read_gbq_table.py @@ -235,6 +235,8 @@ def get_index_cols( | Iterable[int] | int | bigframes.enums.DefaultIndexKind, + *, + names: Optional[Iterable[str]] = None, ) -> List[str]: """ If we can get a total ordering from the table, such as via primary key @@ -245,6 +247,14 @@ def get_index_cols( # Transform index_col -> index_cols so we have a variable that is # always a list of column names (possibly empty). schema_len = len(table.schema) + + # If the `names` is provided, the index_col provided by the user is the new + # name, so we need to rename it to the original name in the table schema. + renamed_schema: Optional[Dict[str, str]] = None + if names is not None: + assert len(list(names)) == schema_len + renamed_schema = {name: field.name for name, field in zip(names, table.schema)} + index_cols: List[str] = [] if isinstance(index_col, bigframes.enums.DefaultIndexKind): if index_col == bigframes.enums.DefaultIndexKind.SEQUENTIAL_INT64: @@ -261,6 +271,8 @@ def get_index_cols( f"Got unexpected index_col {repr(index_col)}. {constants.FEEDBACK_LINK}" ) elif isinstance(index_col, str): + if renamed_schema is not None: + index_col = renamed_schema.get(index_col, index_col) index_cols = [index_col] elif isinstance(index_col, int): if not 0 <= index_col < schema_len: @@ -272,6 +284,8 @@ def get_index_cols( elif isinstance(index_col, Iterable): for item in index_col: if isinstance(item, str): + if renamed_schema is not None: + item = renamed_schema.get(item, item) index_cols.append(item) elif isinstance(item, int): if not 0 <= item < schema_len: diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index 76f12ae438..e6b24e016c 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -348,7 +348,7 @@ def _start_generic_job(self, job: formatting_helpers.GenericJob): def read_gbq_table( self, - query: str, + table_id: str, *, index_col: Iterable[str] | str @@ -356,6 +356,7 @@ def read_gbq_table( | int | bigframes.enums.DefaultIndexKind = (), columns: Iterable[str] = (), + names: Optional[Iterable[str]] = None, max_results: Optional[int] = None, api_name: str = "read_gbq_table", use_cache: bool = True, @@ -375,7 +376,7 @@ def read_gbq_table( ) table_ref = google.cloud.bigquery.table.TableReference.from_string( - query, default_project=self._bqclient.project + table_id, default_project=self._bqclient.project ) columns = list(columns) @@ -411,12 +412,37 @@ def read_gbq_table( f"Column '{key}' of `columns` not found in this table. Did you mean '{possibility}'?" ) + # TODO(b/408499371): check `names` work with `use_cols` for read_csv method. + if names is not None: + len_names = len(list(names)) + len_columns = len(table.schema) + if len_names > len_columns: + raise ValueError( + f"Too many columns specified: expected {len_columns}" + f" and found {len_names}" + ) + elif len_names < len_columns: + if ( + isinstance(index_col, bigframes.enums.DefaultIndexKind) + or index_col != () + ): + raise KeyError( + "When providing both `index_col` and `names`, ensure the " + "number of `names` matches the number of columns in your " + "data." + ) + index_col = range(len_columns - len_names) + names = [ + field.name for field in table.schema[: len_columns - len_names] + ] + list(names) + # Converting index_col into a list of column names requires # the table metadata because we might use the primary keys # when constructing the index. index_cols = bf_read_gbq_table.get_index_cols( table=table, index_col=index_col, + names=names, ) _check_column_duplicates(index_cols, columns) @@ -443,7 +469,7 @@ def read_gbq_table( # TODO(b/338419730): We don't need to fallback to a query for wildcard # tables if we allow some non-determinism when time travel isn't supported. if max_results is not None or bf_io_bigquery.is_table_with_wildcard_suffix( - query + table_id ): # TODO(b/338111344): If we are running a query anyway, we might as # well generate ROW_NUMBER() at the same time. @@ -451,7 +477,7 @@ def read_gbq_table( itertools.chain(index_cols, columns) if columns else () ) query = bf_io_bigquery.to_query( - query, + table_id, columns=all_columns, sql_predicate=bf_io_bigquery.compile_filters(filters) if filters @@ -561,6 +587,15 @@ def read_gbq_table( index_names = [None] value_columns = [col for col in array_value.column_ids if col not in index_cols] + if names is not None: + renamed_cols: Dict[str, str] = { + col: new_name for col, new_name in zip(array_value.column_ids, names) + } + index_names = [ + renamed_cols.get(index_col, index_col) for index_col in index_cols + ] + value_columns = [renamed_cols.get(col, col) for col in value_columns] + block = blocks.Block( array_value, index_columns=index_cols, @@ -576,18 +611,12 @@ def read_gbq_table( df.sort_index() return df - def read_bigquery_load_job( + def load_file( self, filepath_or_buffer: str | IO["bytes"], *, job_config: bigquery.LoadJobConfig, - index_col: Iterable[str] - | str - | Iterable[int] - | int - | bigframes.enums.DefaultIndexKind = (), - columns: Iterable[str] = (), - ) -> dataframe.DataFrame: + ) -> str: # Need to create session table beforehand table = self._storage_manager.create_temp_table(_PLACEHOLDER_SCHEMA) # but, we just overwrite the placeholder schema immediately with the load job @@ -615,16 +644,7 @@ def read_bigquery_load_job( self._start_generic_job(load_job) table_id = f"{table.project}.{table.dataset_id}.{table.table_id}" - - # The BigQuery REST API for tables.get doesn't take a session ID, so we - # can't get the schema for a temp table that way. - - return self.read_gbq_table( - query=table_id, - index_col=index_col, - columns=columns, - api_name="read_gbq_table", - ) + return table_id def read_gbq_query( self, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index c7bf5b3f5e..ced01c940f 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -59,7 +59,7 @@ def df_and_local_csv(scalars_df_index): with tempfile.TemporaryDirectory() as dir: # Prepares local CSV file for reading - path = dir + "/write_df_to_local_csv_file.csv" + path = dir + "/test_read_csv_w_local_csv.csv" scalars_df_index.to_csv(path, index=True) yield scalars_df_index, path @@ -71,7 +71,19 @@ def df_and_gcs_csv(scalars_df_index, gcs_folder): drop_columns = ["bytes_col", "datetime_col", "numeric_col", "geography_col"] scalars_df_index = scalars_df_index.drop(columns=drop_columns) - path = gcs_folder + "test_read_csv_w_write_engine*.csv" + path = gcs_folder + "test_read_csv_w_gcs_csv*.csv" + read_path = utils.get_first_file_from_wildcard(path) + scalars_df_index.to_csv(path, index=True) + return scalars_df_index, read_path + + +@pytest.fixture(scope="module") +def df_and_gcs_csv_for_two_columns(scalars_df_index, gcs_folder): + # Some tests require only two columns to be present in the CSV file. + selected_cols = ["bool_col", "int64_col"] + scalars_df_index = scalars_df_index[selected_cols] + + path = gcs_folder + "df_and_gcs_csv_for_two_columns*.csv" read_path = utils.get_first_file_from_wildcard(path) scalars_df_index.to_csv(path, index=True) return scalars_df_index, read_path @@ -1260,6 +1272,98 @@ def test_read_csv_raises_error_for_invalid_index_col( session.read_csv(path, engine="bigquery", index_col=index_col) +def test_read_csv_for_names(session, df_and_gcs_csv_for_two_columns): + _, path = df_and_gcs_csv_for_two_columns + + names = ["a", "b", "c"] + bf_df = session.read_csv(path, engine="bigquery", names=names) + + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + pd_df = session.read_csv(path, names=names, dtype=bf_df.dtypes.to_dict()) + + assert bf_df.shape == pd_df.shape + assert bf_df.columns.tolist() == pd_df.columns.tolist() + + # BigFrames requires `sort_index()` because BigQuery doesn't preserve row IDs + # (b/280889935) or guarantee row ordering. + bf_df = bf_df.set_index(names[0]).sort_index() + pd_df = pd_df.set_index(names[0]) + pd.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + + +def test_read_csv_for_names_more_than_columns_can_raise_error( + session, df_and_gcs_csv_for_two_columns +): + _, path = df_and_gcs_csv_for_two_columns + names = ["a", "b", "c", "d"] + with pytest.raises( + ValueError, + match="Too many columns specified: expected 3 and found 4", + ): + session.read_csv(path, engine="bigquery", names=names) + + +def test_read_csv_for_names_less_than_columns(session, df_and_gcs_csv_for_two_columns): + _, path = df_and_gcs_csv_for_two_columns + + names = ["b", "c"] + bf_df = session.read_csv(path, engine="bigquery", names=names) + + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + pd_df = session.read_csv(path, names=names, dtype=bf_df.dtypes.to_dict()) + + assert bf_df.shape == pd_df.shape + assert bf_df.columns.tolist() == pd_df.columns.tolist() + + # BigFrames requires `sort_index()` because BigQuery doesn't preserve row IDs + # (b/280889935) or guarantee row ordering. + bf_df = bf_df.sort_index() + + # Pandas's index name is None, while BigFrames's index name is "rowindex". + pd_df.index.name = "rowindex" + pd.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + + +def test_read_csv_for_names_less_than_columns_raise_error_when_index_col_set( + session, df_and_gcs_csv_for_two_columns +): + _, path = df_and_gcs_csv_for_two_columns + + names = ["b", "c"] + with pytest.raises( + KeyError, + match="ensure the number of `names` matches the number of columns in your data.", + ): + session.read_csv(path, engine="bigquery", names=names, index_col="rowindex") + + +@pytest.mark.parametrize( + "index_col", + [ + pytest.param("a", id="single_str"), + pytest.param(["a", "b"], id="multi_str"), + pytest.param(0, id="single_int"), + ], +) +def test_read_csv_for_names_and_index_col( + session, df_and_gcs_csv_for_two_columns, index_col +): + _, path = df_and_gcs_csv_for_two_columns + names = ["a", "b", "c"] + bf_df = session.read_csv(path, engine="bigquery", index_col=index_col, names=names) + + # Convert default pandas dtypes to match BigQuery DataFrames dtypes. + pd_df = session.read_csv( + path, index_col=index_col, names=names, dtype=bf_df.dtypes.to_dict() + ) + + assert bf_df.shape == pd_df.shape + assert bf_df.columns.tolist() == pd_df.columns.tolist() + pd.testing.assert_frame_equal( + bf_df.to_pandas(), pd_df.to_pandas(), check_index_type=False + ) + + @pytest.mark.parametrize( ("kwargs", "match"), [ diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index 22b439a38b..91b6679702 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -108,14 +108,9 @@ @pytest.mark.parametrize( ("kwargs", "match"), [ - pytest.param( - {"engine": "bigquery", "names": []}, - "BigQuery engine does not support these arguments", - id="with_names", - ), pytest.param( {"engine": "bigquery", "dtype": {}}, - "BigQuery engine does not support these arguments", + "BigQuery engine does not support the `dtype` argument", id="with_dtype", ), pytest.param( @@ -203,6 +198,23 @@ def test_read_csv_with_incompatible_write_engine(engine, write_engine): ) +@pytest.mark.parametrize( + ("names", "error_message"), + ( + pytest.param("abc", "Names should be an ordered collection."), + pytest.param({"a", "b", "c"}, "Names should be an ordered collection."), + pytest.param(["a", "a"], "Duplicated names are not allowed."), + ), +) +def test_read_csv_w_bigquery_engine_raises_error_for_invalid_names( + names, error_message +): + session = mocks.create_bigquery_session() + + with pytest.raises(ValueError, match=error_message): + session.read_csv("path/to/csv.csv", engine="bigquery", names=names) + + @pytest.mark.parametrize("missing_parts_table_id", [(""), ("table")]) def test_read_gbq_missing_parts(missing_parts_table_id): session = mocks.create_bigquery_session() diff --git a/third_party/bigframes_vendored/pandas/io/parsers/readers.py b/third_party/bigframes_vendored/pandas/io/parsers/readers.py index 2b1e3dd70b..4757f5ed9d 100644 --- a/third_party/bigframes_vendored/pandas/io/parsers/readers.py +++ b/third_party/bigframes_vendored/pandas/io/parsers/readers.py @@ -114,7 +114,7 @@ def read_csv( names (default None): a list of column names to use. If the file contains a header row and you want to pass this parameter, then `header=0` should be passed as well so the - first (header) row is ignored. Only to be used with default engine. + first (header) row is ignored. index_col (default None): column(s) to use as the row labels of the DataFrame, either given as string name or column index. `index_col=False` can be used with the default From 9ac81351deac9bec8174d3393abdcdef29e30c2a Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Wed, 30 Apr 2025 11:40:55 -0700 Subject: [PATCH 26/28] refactor: implements `compile_selection` method (#1672) --- bigframes/core/compile/sqlglot/compiler.py | 15 ++++++--- .../core/compile/sqlglot/scalar_compiler.py | 33 +++++++++++++++++++ bigframes/core/compile/sqlglot/sqlglot_ir.py | 16 ++++++++- .../test_compile_readlocal/out.sql | 6 +++- 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 bigframes/core/compile/sqlglot/scalar_compiler.py diff --git a/bigframes/core/compile/sqlglot/compiler.py b/bigframes/core/compile/sqlglot/compiler.py index 2b78a6e458..5826ab2871 100644 --- a/bigframes/core/compile/sqlglot/compiler.py +++ b/bigframes/core/compile/sqlglot/compiler.py @@ -20,9 +20,11 @@ from google.cloud import bigquery import pyarrow as pa +import sqlglot.expressions as sge from bigframes.core import expression, identifiers, nodes, rewrite from bigframes.core.compile import configs +import bigframes.core.compile.sqlglot.scalar_compiler as scalar_compiler import bigframes.core.compile.sqlglot.sqlglot_ir as ir import bigframes.core.ordering as bf_ordering @@ -38,7 +40,7 @@ def compile( ordered: bool = True, limit: typing.Optional[int] = None, ) -> str: - """Compile node into sql where rows are sorted with ORDER BY.""" + """Compiles node into sql where rows are sorted with ORDER BY.""" request = configs.CompileRequest(node, sort_rows=ordered, peek_count=limit) return self._compile_sql(request).sql @@ -48,7 +50,7 @@ def compile_raw( ) -> typing.Tuple[ str, typing.Sequence[bigquery.SchemaField], bf_ordering.RowOrdering ]: - """Compile node into sql that exposes all columns, including hidden + """Compiles node into sql that exposes all columns, including hidden ordering-only columns.""" request = configs.CompileRequest( node, sort_rows=False, materialize_all_order_keys=True @@ -163,6 +165,9 @@ def compile_readlocal(node: nodes.ReadLocalNode, *args) -> ir.SQLGlotIR: @_compile_node.register -def compile_selection(node: nodes.SelectionNode, child: ir.SQLGlotIR): - # TODO: add support for selection - return child +def compile_selection(node: nodes.SelectionNode, child: ir.SQLGlotIR) -> ir.SQLGlotIR: + select_cols: typing.Dict[str, sge.Expression] = { + id.name: scalar_compiler.compile_scalar_expression(expr) + for expr, id in node.input_output_pairs + } + return child.select(select_cols) diff --git a/bigframes/core/compile/sqlglot/scalar_compiler.py b/bigframes/core/compile/sqlglot/scalar_compiler.py new file mode 100644 index 0000000000..f9197ef745 --- /dev/null +++ b/bigframes/core/compile/sqlglot/scalar_compiler.py @@ -0,0 +1,33 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import functools + +import sqlglot.expressions as sge + +from bigframes.core import expression + + +@functools.singledispatch +def compile_scalar_expression( + expression: expression.Expression, +) -> sge.Expression: + """Compiles BigFrames scalar expression into SQLGlot expression.""" + raise ValueError(f"Can't compile unrecognized node: {expression}") + + +@compile_scalar_expression.register +def compile_deref_op(expr: expression.DerefOp): + return sge.ColumnDef(this=sge.to_identifier(expr.id.sql, quoted=True)) diff --git a/bigframes/core/compile/sqlglot/sqlglot_ir.py b/bigframes/core/compile/sqlglot/sqlglot_ir.py index 836e7f17ba..fe85f22494 100644 --- a/bigframes/core/compile/sqlglot/sqlglot_ir.py +++ b/bigframes/core/compile/sqlglot/sqlglot_ir.py @@ -30,7 +30,7 @@ class SQLGlotIR: """Helper class to build SQLGlot Query and generate SQL string.""" - expr: sge.Expression = sge.Expression() + expr: sge.Select = sg.select() """The SQLGlot expression representing the query.""" dialect = sqlglot.dialects.bigquery.BigQuery @@ -90,6 +90,20 @@ def from_pandas( ) return cls(expr=sg.select(sge.Star()).from_(expr)) + def select( + self, + select_cols: typing.Dict[str, sge.Expression], + ) -> SQLGlotIR: + selected_cols = [ + sge.Alias( + this=expr, + alias=sge.to_identifier(id, quoted=self.quoted), + ) + for id, expr in select_cols.items() + ] + expr = self.expr.select(*selected_cols, append=False) + return SQLGlotIR(expr=expr) + def _literal(value: typing.Any, dtype: str) -> sge.Expression: if value is None: diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql index 5c22910422..c2a443dab2 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql @@ -1,3 +1,7 @@ SELECT - * + `bfcol_0` AS `bfcol_5`, + `bfcol_1` AS `bfcol_6`, + `bfcol_2` AS `bfcol_7`, + `bfcol_3` AS `bfcol_8`, + `bfcol_4` AS `bfcol_9` FROM UNNEST(ARRAY>[(1, -10, TRUE, 'b', 0), (2, 20, CAST(NULL AS BOOLEAN), 'aa', 1), (3, 30, FALSE, 'ccc', 2)]) \ No newline at end of file From 36a3314aba36d6699d12ebfe905dbaac4ee1f4cd Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Wed, 30 Apr 2025 13:25:41 -0700 Subject: [PATCH 27/28] refactor: support all data types for readlocal compiler (#1666) --- bigframes/core/compile/sqlglot/compiler.py | 11 +- bigframes/core/compile/sqlglot/sqlglot_ir.py | 68 +++++-- tests/data/scalars.jsonl | 18 +- tests/unit/core/compile/sqlglot/conftest.py | 92 ++++++++- .../test_compile_readlocal/out.sql | 176 +++++++++++++++++- .../test_compile_readlocal_w_json_df/out.sql | 4 + .../test_compile_readlocal_w_lists_df/out.sql | 41 ++++ .../out.sql | 19 ++ .../out.sql | 21 +++ .../compile/sqlglot/test_compile_readlocal.py | 31 ++- .../sql/compilers/bigquery/__init__.py | 1 - 11 files changed, 427 insertions(+), 55 deletions(-) create mode 100644 tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_json_df/out.sql create mode 100644 tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_lists_df/out.sql create mode 100644 tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_nested_structs_df/out.sql create mode 100644 tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_structs_df/out.sql diff --git a/bigframes/core/compile/sqlglot/compiler.py b/bigframes/core/compile/sqlglot/compiler.py index 5826ab2871..cb510ce365 100644 --- a/bigframes/core/compile/sqlglot/compiler.py +++ b/bigframes/core/compile/sqlglot/compiler.py @@ -146,22 +146,17 @@ def _compile_node( @_compile_node.register def compile_readlocal(node: nodes.ReadLocalNode, *args) -> ir.SQLGlotIR: - offsets = node.offsets_col.sql if node.offsets_col else None - schema_names = node.schema.names - schema_dtypes = node.schema.dtypes - pa_table = node.local_data_source.data pa_table = pa_table.select([item.source_id for item in node.scan_list.items]) - pa_table = pa_table.rename_columns( - {item.source_id: item.id.sql for item in node.scan_list.items} - ) + pa_table = pa_table.rename_columns([item.id.sql for item in node.scan_list.items]) + offsets = node.offsets_col.sql if node.offsets_col else None if offsets: pa_table = pa_table.append_column( offsets, pa.array(range(pa_table.num_rows), type=pa.int64()) ) - return ir.SQLGlotIR.from_pandas(pa_table.to_pandas(), schema_names, schema_dtypes) + return ir.SQLGlotIR.from_pyarrow(pa_table, node.schema) @_compile_node.register diff --git a/bigframes/core/compile/sqlglot/sqlglot_ir.py b/bigframes/core/compile/sqlglot/sqlglot_ir.py index fe85f22494..607e712a2b 100644 --- a/bigframes/core/compile/sqlglot/sqlglot_ir.py +++ b/bigframes/core/compile/sqlglot/sqlglot_ir.py @@ -17,13 +17,23 @@ import dataclasses import typing -import pandas as pd +import pyarrow as pa import sqlglot as sg import sqlglot.dialects.bigquery import sqlglot.expressions as sge from bigframes import dtypes import bigframes.core.compile.sqlglot.sqlglot_types as sgt +import bigframes.core.local_data as local_data +import bigframes.core.schema as schemata + +# shapely.wkt.dumps was moved to shapely.io.to_wkt in 2.0. +try: + from shapely.io import to_wkt # type: ignore +except ImportError: + from shapely.wkt import dumps # type: ignore + + to_wkt = dumps @dataclasses.dataclass(frozen=True) @@ -48,35 +58,32 @@ def sql(self) -> str: return self.expr.sql(dialect=self.dialect, pretty=self.pretty) @classmethod - def from_pandas( - cls, - pd_df: pd.DataFrame, - schema_names: typing.Sequence[str], - schema_dtypes: typing.Sequence[dtypes.Dtype], + def from_pyarrow( + cls, pa_table: pa.Table, schema: schemata.ArraySchema ) -> SQLGlotIR: """Builds SQLGlot expression from pyarrow table.""" dtype_expr = sge.DataType( this=sge.DataType.Type.STRUCT, expressions=[ sge.ColumnDef( - this=sge.to_identifier(name, quoted=True), - kind=sgt.SQLGlotType.from_bigframes_dtype(dtype), + this=sge.to_identifier(field.column, quoted=True), + kind=sgt.SQLGlotType.from_bigframes_dtype(field.dtype), ) - for name, dtype in zip(schema_names, schema_dtypes) + for field in schema.items ], nested=True, ) data_expr = [ - sge.Tuple( + sge.Struct( expressions=tuple( _literal( value=value, - dtype=sgt.SQLGlotType.from_bigframes_dtype(dtype), + dtype=field.dtype, ) - for value, dtype in zip(row, schema_dtypes) + for value, field in zip(tuple(row_dict.values()), schema.items) ) ) - for _, row in pd_df.iterrows() + for row_dict in local_data._iter_table(pa_table, schema) ] expr = sge.Unnest( expressions=[ @@ -105,13 +112,36 @@ def select( return SQLGlotIR(expr=expr) -def _literal(value: typing.Any, dtype: str) -> sge.Expression: +def _literal(value: typing.Any, dtype: dtypes.Dtype) -> sge.Expression: + sqlglot_type = sgt.SQLGlotType.from_bigframes_dtype(dtype) if value is None: - return _cast(sge.Null(), dtype) - - # TODO: handle other types like visit_DefaultLiteral - return sge.convert(value) + return _cast(sge.Null(), sqlglot_type) + elif dtype == dtypes.BYTES_DTYPE: + return _cast(str(value), sqlglot_type) + elif dtypes.is_time_like(dtype): + return _cast(sge.convert(value.isoformat()), sqlglot_type) + elif dtypes.is_geo_like(dtype): + wkt = value if isinstance(value, str) else to_wkt(value) + return sge.func("ST_GEOGFROMTEXT", sge.convert(wkt)) + elif dtype == dtypes.JSON_DTYPE: + return sge.ParseJSON(this=sge.convert(str(value))) + elif dtypes.is_struct_like(dtype): + items = [ + _literal(value=value[field_name], dtype=field_dtype).as_( + field_name, quoted=True + ) + for field_name, field_dtype in dtypes.get_struct_fields(dtype).items() + ] + return sge.Struct.from_arg_list(items) + elif dtypes.is_array_like(dtype): + value_type = dtypes.get_array_inner_type(dtype) + values = sge.Array( + expressions=[_literal(value=v, dtype=value_type) for v in value] + ) + return values if len(value) > 0 else _cast(values, sqlglot_type) + else: + return sge.convert(value) -def _cast(arg, to) -> sge.Cast: +def _cast(arg: typing.Any, to: str) -> sge.Cast: return sge.Cast(this=arg, to=to) diff --git a/tests/data/scalars.jsonl b/tests/data/scalars.jsonl index 03755c94b7..2e5a1499b9 100644 --- a/tests/data/scalars.jsonl +++ b/tests/data/scalars.jsonl @@ -1,9 +1,9 @@ -{"bool_col": true, "bytes_col": "SGVsbG8sIFdvcmxkIQ==", "date_col": "2021-07-21", "datetime_col": "2021-07-21 11:39:45", "geography_col": "POINT(-122.0838511 37.3860517)", "int64_col": "123456789", "int64_too": "0", "numeric_col": "1.23456789", "float64_col": "1.25", "rowindex": 0, "rowindex_2": 0, "string_col": "Hello, World!", "time_col": "11:41:43.076160", "timestamp_col": "2021-07-21T17:43:43.945289Z"} -{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "1991-02-03", "datetime_col": "1991-01-02 03:45:06", "geography_col": "POINT(-71.104 42.315)", "int64_col": "-987654321", "int64_too": "1", "numeric_col": "1.23456789", "float64_col": "2.51", "rowindex": 1, "rowindex_2": 1, "string_col": "こんにちは", "time_col": "11:14:34.701606", "timestamp_col": "2021-07-21T17:43:43.945289Z"} -{"bool_col": true, "bytes_col": "wqFIb2xhIE11bmRvIQ==", "date_col": "2023-03-01", "datetime_col": "2023-03-01 10:55:13", "geography_col": "POINT(-0.124474760143016 51.5007826749545)", "int64_col": "314159", "int64_too": "0", "numeric_col": "101.1010101", "float64_col": "2.5e10", "rowindex": 2, "rowindex_2": 2, "string_col": " ¡Hola Mundo! ", "time_col": "23:59:59.999999", "timestamp_col": "2023-03-01T10:55:13.250125Z"} -{"bool_col": null, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": null, "int64_too": "1", "numeric_col": null, "float64_col": null, "rowindex": 3, "rowindex_2": 3, "string_col": null, "time_col": null, "timestamp_col": null} -{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "2021-07-21", "datetime_col": null, "geography_col": null, "int64_col": "-234892", "int64_too": "-2345", "numeric_col": null, "float64_col": null, "rowindex": 4, "rowindex_2": 4, "string_col": "Hello, World!", "time_col": null, "timestamp_col": null} -{"bool_col": false, "bytes_col": "R8O8dGVuIFRhZw==", "date_col": "1980-03-14", "datetime_col": "1980-03-14 15:16:17", "geography_col": null, "int64_col": "55555", "int64_too": "0", "numeric_col": "5.555555", "float64_col": "555.555", "rowindex": 5, "rowindex_2": 5, "string_col": "Güten Tag!", "time_col": "15:16:17.181921", "timestamp_col": "1980-03-14T15:16:17.181921Z"} -{"bool_col": true, "bytes_col": "SGVsbG8JQmlnRnJhbWVzIQc=", "date_col": "2023-05-23", "datetime_col": "2023-05-23 11:37:01", "geography_col": "MULTIPOINT (20 20, 10 40, 40 30, 30 10)", "int64_col": "101202303", "int64_too": "2", "numeric_col": "-10.090807", "float64_col": "-123.456", "rowindex": 6, "rowindex_2": 6, "string_col": "capitalize, This ", "time_col": "01:02:03.456789", "timestamp_col": "2023-05-23T11:42:55.000001Z"} -{"bool_col": true, "bytes_col": null, "date_col": "2038-01-20", "datetime_col": "2038-01-19 03:14:08", "geography_col": null, "int64_col": "-214748367", "int64_too": "2", "numeric_col": "11111111.1", "float64_col": "42.42", "rowindex": 7, "rowindex_2": 7, "string_col": " سلام", "time_col": "12:00:00.000001", "timestamp_col": "2038-01-19T03:14:17.999999Z"} -{"bool_col": false, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": "2", "int64_too": "1", "numeric_col": null, "float64_col": "6.87", "rowindex": 8, "rowindex_2": 8, "string_col": "T", "time_col": null, "timestamp_col": null} \ No newline at end of file +{"bool_col": true, "bytes_col": "SGVsbG8sIFdvcmxkIQ==", "date_col": "2021-07-21", "datetime_col": "2021-07-21 11:39:45", "geography_col": "POINT(-122.0838511 37.3860517)", "int64_col": "123456789", "int64_too": "0", "numeric_col": "1.23456789", "float64_col": "1.25", "rowindex": 0, "rowindex_2": 0, "string_col": "Hello, World!", "time_col": "11:41:43.076160", "timestamp_col": "2021-07-21T17:43:43.945289Z"} +{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "1991-02-03", "datetime_col": "1991-01-02 03:45:06", "geography_col": "POINT(-71.104 42.315)", "int64_col": "-987654321", "int64_too": "1", "numeric_col": "1.23456789", "float64_col": "2.51", "rowindex": 1, "rowindex_2": 1, "string_col": "こんにちは", "time_col": "11:14:34.701606", "timestamp_col": "2021-07-21T17:43:43.945289Z"} +{"bool_col": true, "bytes_col": "wqFIb2xhIE11bmRvIQ==", "date_col": "2023-03-01", "datetime_col": "2023-03-01 10:55:13", "geography_col": "POINT(-0.124474760143016 51.5007826749545)", "int64_col": "314159", "int64_too": "0", "numeric_col": "101.1010101", "float64_col": "2.5e10", "rowindex": 2, "rowindex_2": 2, "string_col": " ¡Hola Mundo! ", "time_col": "23:59:59.999999", "timestamp_col": "2023-03-01T10:55:13.250125Z"} +{"bool_col": null, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": null, "int64_too": "1", "numeric_col": null, "float64_col": null, "rowindex": 3, "rowindex_2": 3, "string_col": null, "time_col": null, "timestamp_col": null} +{"bool_col": false, "bytes_col": "44GT44KT44Gr44Gh44Gv", "date_col": "2021-07-21", "datetime_col": null, "geography_col": null, "int64_col": "-234892", "int64_too": "-2345", "numeric_col": null, "float64_col": null, "rowindex": 4, "rowindex_2": 4, "string_col": "Hello, World!", "time_col": null, "timestamp_col": null} +{"bool_col": false, "bytes_col": "R8O8dGVuIFRhZw==", "date_col": "1980-03-14", "datetime_col": "1980-03-14 15:16:17", "geography_col": null, "int64_col": "55555", "int64_too": "0", "numeric_col": "5.555555", "float64_col": "555.555", "rowindex": 5, "rowindex_2": 5, "string_col": "Güten Tag!", "time_col": "15:16:17.181921", "timestamp_col": "1980-03-14T15:16:17.181921Z"} +{"bool_col": true, "bytes_col": "SGVsbG8JQmlnRnJhbWVzIQc=", "date_col": "2023-05-23", "datetime_col": "2023-05-23 11:37:01", "geography_col": "LINESTRING(-0.127959 51.507728, -0.127026 51.507473)", "int64_col": "101202303", "int64_too": "2", "numeric_col": "-10.090807", "float64_col": "-123.456", "rowindex": 6, "rowindex_2": 6, "string_col": "capitalize, This ", "time_col": "01:02:03.456789", "timestamp_col": "2023-05-23T11:42:55.000001Z"} +{"bool_col": true, "bytes_col": null, "date_col": "2038-01-20", "datetime_col": "2038-01-19 03:14:08", "geography_col": null, "int64_col": "-214748367", "int64_too": "2", "numeric_col": "11111111.1", "float64_col": "42.42", "rowindex": 7, "rowindex_2": 7, "string_col": " سلام", "time_col": "12:00:00.000001", "timestamp_col": "2038-01-19T03:14:17.999999Z"} +{"bool_col": false, "bytes_col": null, "date_col": null, "datetime_col": null, "geography_col": null, "int64_col": "2", "int64_too": "1", "numeric_col": null, "float64_col": "6.87", "rowindex": 8, "rowindex_2": 8, "string_col": "T", "time_col": null, "timestamp_col": null} \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/conftest.py b/tests/unit/core/compile/sqlglot/conftest.py index 23456fc76c..4d871fd707 100644 --- a/tests/unit/core/compile/sqlglot/conftest.py +++ b/tests/unit/core/compile/sqlglot/conftest.py @@ -12,29 +12,101 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pathlib + import pandas as pd +import pyarrow as pa import pytest +from bigframes import dtypes +import tests.system.utils + +CURRENT_DIR = pathlib.Path(__file__).parent +DATA_DIR = CURRENT_DIR.parent.parent.parent.parent / "data" + -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def compiler_session(): from . import compiler_session return compiler_session.SQLCompilerSession() -@pytest.fixture(scope="module") -def all_types_df() -> pd.DataFrame: - # TODO: all types pandas dataframes +@pytest.fixture(scope="session") +def scalars_types_pandas_df() -> pd.DataFrame: + """Returns a pandas DataFrame containing all scalar types and using the `rowindex` + column as the index.""" # TODO: add tests for empty dataframes + df = pd.read_json( + DATA_DIR / "scalars.jsonl", + lines=True, + ) + tests.system.utils.convert_pandas_dtypes(df, bytes_col=True) + + df = df.set_index("rowindex", drop=False) + return df + + +@pytest.fixture(scope="session") +def nested_structs_pandas_df() -> pd.DataFrame: + """Returns a pandas DataFrame containing STRUCT types and using the `id` + column as the index.""" + + df = pd.read_json( + DATA_DIR / "nested_structs.jsonl", + lines=True, + ) + df = df.set_index("id") + + address_struct_schema = pa.struct( + [pa.field("city", pa.string()), pa.field("country", pa.string())] + ) + person_struct_schema = pa.struct( + [ + pa.field("name", pa.string()), + pa.field("age", pa.int64()), + pa.field("address", address_struct_schema), + ] + ) + df["person"] = df["person"].astype(pd.ArrowDtype(person_struct_schema)) + return df + + +@pytest.fixture(scope="session") +def repeated_pandas_df() -> pd.DataFrame: + """Returns a pandas DataFrame containing LIST types and using the `rowindex` + column as the index.""" + + df = pd.read_json( + DATA_DIR / "repeated.jsonl", + lines=True, + ) + df = df.set_index("rowindex") + return df + + +@pytest.fixture(scope="session") +def json_pandas_df() -> pd.DataFrame: + """Returns a pandas DataFrame containing JSON types and using the `rowindex` + column as the index.""" + json_data = [ + "null", + "true", + "100", + "0.98", + '"a string"', + "[]", + "[1, 2, 3]", + '[{"a": 1}, {"a": 2}, {"a": null}, {}]', + '"100"', + '{"date": "2024-07-16"}', + '{"int_value": 2, "null_filed": null}', + '{"list_data": [10, 20, 30]}', + ] df = pd.DataFrame( { - "int1": pd.Series([1, 2, 3], dtype="Int64"), - "int2": pd.Series([-10, 20, 30], dtype="Int64"), - "bools": pd.Series([True, None, False], dtype="boolean"), - "strings": pd.Series(["b", "aa", "ccc"], dtype="string[pyarrow]"), + "json_col": pd.Series(json_data, dtype=dtypes.JSON_DTYPE), }, + index=pd.Series(range(len(json_data)), dtype=dtypes.INT_DTYPE), ) - # add more complexity index. - df.index = df.index.astype("Int64") return df diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql index c2a443dab2..0ef80dc8b0 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal/out.sql @@ -1,7 +1,171 @@ SELECT - `bfcol_0` AS `bfcol_5`, - `bfcol_1` AS `bfcol_6`, - `bfcol_2` AS `bfcol_7`, - `bfcol_3` AS `bfcol_8`, - `bfcol_4` AS `bfcol_9` -FROM UNNEST(ARRAY>[(1, -10, TRUE, 'b', 0), (2, 20, CAST(NULL AS BOOLEAN), 'aa', 1), (3, 30, FALSE, 'ccc', 2)]) \ No newline at end of file + `bfcol_0` AS `bfcol_16`, + `bfcol_1` AS `bfcol_17`, + `bfcol_2` AS `bfcol_18`, + `bfcol_3` AS `bfcol_19`, + `bfcol_4` AS `bfcol_20`, + `bfcol_5` AS `bfcol_21`, + `bfcol_6` AS `bfcol_22`, + `bfcol_7` AS `bfcol_23`, + `bfcol_8` AS `bfcol_24`, + `bfcol_9` AS `bfcol_25`, + `bfcol_10` AS `bfcol_26`, + `bfcol_11` AS `bfcol_27`, + `bfcol_12` AS `bfcol_28`, + `bfcol_13` AS `bfcol_29`, + `bfcol_14` AS `bfcol_30`, + `bfcol_15` AS `bfcol_31` +FROM UNNEST(ARRAY>[STRUCT( + 0, + TRUE, + CAST(b'Hello, World!' AS BYTES), + CAST('2021-07-21' AS DATE), + CAST('2021-07-21T11:39:45' AS DATETIME), + ST_GEOGFROMTEXT('POINT (-122.0838511 37.3860517)'), + 123456789, + 0, + 1.234567890, + 1.25, + 0, + 0, + 'Hello, World!', + CAST('11:41:43.076160' AS TIME), + CAST('2021-07-21T17:43:43.945289+00:00' AS TIMESTAMP), + 0 +), STRUCT( + 1, + FALSE, + CAST(b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf' AS BYTES), + CAST('1991-02-03' AS DATE), + CAST('1991-01-02T03:45:06' AS DATETIME), + ST_GEOGFROMTEXT('POINT (-71.104 42.315)'), + -987654321, + 1, + 1.234567890, + 2.51, + 1, + 1, + 'こんにちは', + CAST('11:14:34.701606' AS TIME), + CAST('2021-07-21T17:43:43.945289+00:00' AS TIMESTAMP), + 1 +), STRUCT( + 2, + TRUE, + CAST(b'\xc2\xa1Hola Mundo!' AS BYTES), + CAST('2023-03-01' AS DATE), + CAST('2023-03-01T10:55:13' AS DATETIME), + ST_GEOGFROMTEXT('POINT (-0.124474760143016 51.5007826749545)'), + 314159, + 0, + 101.101010100, + 25000000000.0, + 2, + 2, + ' ¡Hola Mundo! ', + CAST('23:59:59.999999' AS TIME), + CAST('2023-03-01T10:55:13.250125+00:00' AS TIMESTAMP), + 2 +), STRUCT( + 3, + CAST(NULL AS BOOLEAN), + CAST(NULL AS BYTES), + CAST(NULL AS DATE), + CAST(NULL AS DATETIME), + CAST(NULL AS GEOGRAPHY), + CAST(NULL AS INT64), + 1, + CAST(NULL AS NUMERIC), + CAST(NULL AS FLOAT64), + 3, + 3, + CAST(NULL AS STRING), + CAST(NULL AS TIME), + CAST(NULL AS TIMESTAMP), + 3 +), STRUCT( + 4, + FALSE, + CAST(b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf' AS BYTES), + CAST('2021-07-21' AS DATE), + CAST(NULL AS DATETIME), + CAST(NULL AS GEOGRAPHY), + -234892, + -2345, + CAST(NULL AS NUMERIC), + CAST(NULL AS FLOAT64), + 4, + 4, + 'Hello, World!', + CAST(NULL AS TIME), + CAST(NULL AS TIMESTAMP), + 4 +), STRUCT( + 5, + FALSE, + CAST(b'G\xc3\xbcten Tag' AS BYTES), + CAST('1980-03-14' AS DATE), + CAST('1980-03-14T15:16:17' AS DATETIME), + CAST(NULL AS GEOGRAPHY), + 55555, + 0, + 5.555555000, + 555.555, + 5, + 5, + 'Güten Tag!', + CAST('15:16:17.181921' AS TIME), + CAST('1980-03-14T15:16:17.181921+00:00' AS TIMESTAMP), + 5 +), STRUCT( + 6, + TRUE, + CAST(b'Hello\tBigFrames!\x07' AS BYTES), + CAST('2023-05-23' AS DATE), + CAST('2023-05-23T11:37:01' AS DATETIME), + ST_GEOGFROMTEXT('LINESTRING (-0.127959 51.507728, -0.127026 51.507473)'), + 101202303, + 2, + -10.090807000, + -123.456, + 6, + 6, + 'capitalize, This ', + CAST('01:02:03.456789' AS TIME), + CAST('2023-05-23T11:42:55.000001+00:00' AS TIMESTAMP), + 6 +), STRUCT( + 7, + TRUE, + CAST(NULL AS BYTES), + CAST('2038-01-20' AS DATE), + CAST('2038-01-19T03:14:08' AS DATETIME), + CAST(NULL AS GEOGRAPHY), + -214748367, + 2, + 11111111.100000000, + 42.42, + 7, + 7, + ' سلام', + CAST('12:00:00.000001' AS TIME), + CAST('2038-01-19T03:14:17.999999+00:00' AS TIMESTAMP), + 7 +), STRUCT( + 8, + FALSE, + CAST(NULL AS BYTES), + CAST(NULL AS DATE), + CAST(NULL AS DATETIME), + CAST(NULL AS GEOGRAPHY), + 2, + 1, + CAST(NULL AS NUMERIC), + 6.87, + 8, + 8, + 'T', + CAST(NULL AS TIME), + CAST(NULL AS TIMESTAMP), + 8 +)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_json_df/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_json_df/out.sql new file mode 100644 index 0000000000..3b780e6d8e --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_json_df/out.sql @@ -0,0 +1,4 @@ +SELECT + `bfcol_0` AS `bfcol_2`, + `bfcol_1` AS `bfcol_3` +FROM UNNEST(ARRAY>[STRUCT(PARSE_JSON('null'), 0), STRUCT(PARSE_JSON('true'), 1), STRUCT(PARSE_JSON('100'), 2), STRUCT(PARSE_JSON('0.98'), 3), STRUCT(PARSE_JSON('"a string"'), 4), STRUCT(PARSE_JSON('[]'), 5), STRUCT(PARSE_JSON('[1,2,3]'), 6), STRUCT(PARSE_JSON('[{"a":1},{"a":2},{"a":null},{}]'), 7), STRUCT(PARSE_JSON('"100"'), 8), STRUCT(PARSE_JSON('{"date":"2024-07-16"}'), 9), STRUCT(PARSE_JSON('{"int_value":2,"null_filed":null}'), 10), STRUCT(PARSE_JSON('{"list_data":[10,20,30]}'), 11)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_lists_df/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_lists_df/out.sql new file mode 100644 index 0000000000..6998b41b27 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_lists_df/out.sql @@ -0,0 +1,41 @@ +SELECT + `bfcol_0` AS `bfcol_9`, + `bfcol_1` AS `bfcol_10`, + `bfcol_2` AS `bfcol_11`, + `bfcol_3` AS `bfcol_12`, + `bfcol_4` AS `bfcol_13`, + `bfcol_5` AS `bfcol_14`, + `bfcol_6` AS `bfcol_15`, + `bfcol_7` AS `bfcol_16`, + `bfcol_8` AS `bfcol_17` +FROM UNNEST(ARRAY, `bfcol_2` ARRAY, `bfcol_3` ARRAY, `bfcol_4` ARRAY, `bfcol_5` ARRAY, `bfcol_6` ARRAY, `bfcol_7` ARRAY, `bfcol_8` INT64>>[STRUCT( + 0, + [1], + [TRUE], + [1.2, 2.3], + ['2021-07-21'], + ['2021-07-21 11:39:45'], + [1.2, 2.3, 3.4], + ['abc', 'de', 'f'], + 0 +), STRUCT( + 1, + [1, 2], + [TRUE, FALSE], + [1.1], + ['2021-07-21', '1987-03-28'], + ['1999-03-14 17:22:00'], + [5.5, 2.3], + ['a', 'bc', 'de'], + 1 +), STRUCT( + 2, + [1, 2, 3], + [TRUE], + [0.5, -1.9, 2.3], + ['2017-08-01', '2004-11-22'], + ['1979-06-03 03:20:45'], + [1.7000000000000002], + ['', 'a'], + 2 +)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_nested_structs_df/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_nested_structs_df/out.sql new file mode 100644 index 0000000000..42b7bc7361 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_nested_structs_df/out.sql @@ -0,0 +1,19 @@ +SELECT + * +FROM UNNEST(ARRAY>, `bfcol_2` INT64>>[( + 1, + STRUCT( + 'Alice' AS `name`, + 30 AS `age`, + STRUCT('New York' AS `city`, 'USA' AS `country`) AS `address` + ), + 0 +), ( + 2, + STRUCT( + 'Bob' AS `name`, + 25 AS `age`, + STRUCT('London' AS `city`, 'UK' AS `country`) AS `address` + ), + 1 +)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_structs_df/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_structs_df/out.sql new file mode 100644 index 0000000000..99b94915bf --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readlocal/test_compile_readlocal_w_structs_df/out.sql @@ -0,0 +1,21 @@ +SELECT + `bfcol_0` AS `bfcol_3`, + `bfcol_1` AS `bfcol_4`, + `bfcol_2` AS `bfcol_5` +FROM UNNEST(ARRAY>, `bfcol_2` INT64>>[STRUCT( + 1, + STRUCT( + 'Alice' AS `name`, + 30 AS `age`, + STRUCT('New York' AS `city`, 'USA' AS `country`) AS `address` + ), + 0 +), STRUCT( + 2, + STRUCT( + 'Bob' AS `name`, + 25 AS `age`, + STRUCT('London' AS `city`, 'UK' AS `country`) AS `address` + ), + 1 +)]) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/test_compile_readlocal.py b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py index ad7cc5785a..58587da129 100644 --- a/tests/unit/core/compile/sqlglot/test_compile_readlocal.py +++ b/tests/unit/core/compile/sqlglot/test_compile_readlocal.py @@ -22,7 +22,34 @@ def test_compile_readlocal( - all_types_df: pd.DataFrame, compiler_session: bigframes.Session, snapshot + scalars_types_pandas_df: pd.DataFrame, compiler_session: bigframes.Session, snapshot ): - bf_df = bpd.DataFrame(all_types_df, session=compiler_session) + bf_df = bpd.DataFrame(scalars_types_pandas_df, session=compiler_session) + snapshot.assert_match(bf_df.sql, "out.sql") + + +def test_compile_readlocal_w_structs_df( + nested_structs_pandas_df: pd.DataFrame, + compiler_session: bigframes.Session, + snapshot, +): + bf_df = bpd.DataFrame(nested_structs_pandas_df, session=compiler_session) + snapshot.assert_match(bf_df.sql, "out.sql") + + +def test_compile_readlocal_w_lists_df( + repeated_pandas_df: pd.DataFrame, + compiler_session: bigframes.Session, + snapshot, +): + bf_df = bpd.DataFrame(repeated_pandas_df, session=compiler_session) + snapshot.assert_match(bf_df.sql, "out.sql") + + +def test_compile_readlocal_w_json_df( + json_pandas_df: pd.DataFrame, + compiler_session: bigframes.Session, + snapshot, +): + bf_df = bpd.DataFrame(json_pandas_df, session=compiler_session) snapshot.assert_match(bf_df.sql, "out.sql") diff --git a/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py index 7e001d1ac3..be8f9fc555 100644 --- a/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py +++ b/third_party/bigframes_vendored/ibis/backends/sql/compilers/bigquery/__init__.py @@ -1067,7 +1067,6 @@ def visit_InMemoryTable(self, op, *, name, schema, data): columns=columns, ), ) - # return expr return sg.select(sge.Star()).from_(expr) def visit_ArrayAggregate(self, op, *, arg, order_by, where): From f3fd7e226ec28f7c01370adb6f2c250432b7e4b3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:59:59 -0700 Subject: [PATCH 28/28] chore(main): release 2.2.0 (#1643) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 43 +++++++++++++++++++++++ bigframes/version.py | 4 +-- third_party/bigframes_vendored/version.py | 4 +-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1e331d1d..b6c08af05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ [1]: https://pypi.org/project/bigframes/#history +## [2.2.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.1.0...v2.2.0) (2025-04-30) + + +### Features + +* Add gemini-2.0-flash-001 and gemini-2.0-flash-lite-001 to fine tune score endponts and multimodal endpoints ([#1650](https://github.com/googleapis/python-bigquery-dataframes/issues/1650)) ([4fb54df](https://github.com/googleapis/python-bigquery-dataframes/commit/4fb54dfe448604a90fc1818cf18b1e77e1e7227b)) +* Add GeminiTextGenerator.predict structured output ([#1653](https://github.com/googleapis/python-bigquery-dataframes/issues/1653)) ([6199023](https://github.com/googleapis/python-bigquery-dataframes/commit/6199023a6a71e72e926f5879e74a15215bc6e4a0)) +* DataFrames.__getitem__ support for slice input ([#1668](https://github.com/googleapis/python-bigquery-dataframes/issues/1668)) ([563f0cb](https://github.com/googleapis/python-bigquery-dataframes/commit/563f0cbdf4a18c3cd1bd2a4b52de823165638911)) +* Print right origin of `PreviewWarning` for the `bpd.udf` ([#1629](https://github.com/googleapis/python-bigquery-dataframes/issues/1629)) ([48d10d1](https://github.com/googleapis/python-bigquery-dataframes/commit/48d10d1f0150a29dd3b91f505f8d3874e0b88c42)) +* Session.bytes_processed_sum will be updated when allow_large_re… ([#1669](https://github.com/googleapis/python-bigquery-dataframes/issues/1669)) ([ae312db](https://github.com/googleapis/python-bigquery-dataframes/commit/ae312dbed25da6da5e2817d5c9838654c2a1ad1c)) +* Short circuit query for local scan ([#1618](https://github.com/googleapis/python-bigquery-dataframes/issues/1618)) ([e84f232](https://github.com/googleapis/python-bigquery-dataframes/commit/e84f232b0fc5e2167a7cddb355cf0c8837ae5422)) +* Support names parameter in read_csv for bigquery engine ([#1659](https://github.com/googleapis/python-bigquery-dataframes/issues/1659)) ([3388191](https://github.com/googleapis/python-bigquery-dataframes/commit/33881914ab5b8d0e701eabd9c731aed1deab3d49)) +* Support passing list of values to bigframes.core.sql.simple_literal ([#1641](https://github.com/googleapis/python-bigquery-dataframes/issues/1641)) ([102d363](https://github.com/googleapis/python-bigquery-dataframes/commit/102d363aa7e3245ff262c817bc756ea0eaee57e7)) +* Support write api as loading option ([#1617](https://github.com/googleapis/python-bigquery-dataframes/issues/1617)) ([c46ad06](https://github.com/googleapis/python-bigquery-dataframes/commit/c46ad0647785a9207359eba0fb5b6f7a16610f2a)) + + +### Bug Fixes + +* DataFrame accessors is not pupulated ([#1639](https://github.com/googleapis/python-bigquery-dataframes/issues/1639)) ([28afa2c](https://github.com/googleapis/python-bigquery-dataframes/commit/28afa2c73c0517f9365fab05193706631b656551)) +* Prefer remote schema instead of throwing on materialize conflicts ([#1644](https://github.com/googleapis/python-bigquery-dataframes/issues/1644)) ([53fc25b](https://github.com/googleapis/python-bigquery-dataframes/commit/53fc25bfc86e166b91e5001506051b1cac34c996)) +* Remove itertools.pairwise usage ([#1638](https://github.com/googleapis/python-bigquery-dataframes/issues/1638)) ([9662745](https://github.com/googleapis/python-bigquery-dataframes/commit/9662745265c8c6e42f372629bd2c7806542cee1a)) +* Resolve issue where pre-release versions of google-auth are installed ([#1491](https://github.com/googleapis/python-bigquery-dataframes/issues/1491)) ([ebb7a5e](https://github.com/googleapis/python-bigquery-dataframes/commit/ebb7a5e2b24fa57d6fe6a76d9b857ad44c67d194)) +* Resolve some of the typo errors ([#1655](https://github.com/googleapis/python-bigquery-dataframes/issues/1655)) ([cd7fbde](https://github.com/googleapis/python-bigquery-dataframes/commit/cd7fbde026522f53a23a4bb6585ad8629769fad1)) + + +### Performance Improvements + +* Fold row count ops when known ([#1656](https://github.com/googleapis/python-bigquery-dataframes/issues/1656)) ([c958dbe](https://github.com/googleapis/python-bigquery-dataframes/commit/c958dbea32b77cec9fddfc09e3b40d1da220a42c)) +* Use flyweight for node fields ([#1654](https://github.com/googleapis/python-bigquery-dataframes/issues/1654)) ([8482bfc](https://github.com/googleapis/python-bigquery-dataframes/commit/8482bfc1d4caa91a35c4fbf0be420301d05ad544)) + + +### Dependencies + +* Support shapely 1.8.5+ again ([#1651](https://github.com/googleapis/python-bigquery-dataframes/issues/1651)) ([ae83e61](https://github.com/googleapis/python-bigquery-dataframes/commit/ae83e61c49ade64d6f727e9f364bd2f1aeec6e19)) + + +### Documentation + +* Add JSON data types notebook ([#1647](https://github.com/googleapis/python-bigquery-dataframes/issues/1647)) ([9128c4a](https://github.com/googleapis/python-bigquery-dataframes/commit/9128c4a31dab487bc23f67c43380abd0beda5b1c)) +* Add sample code snippets for `udf` ([#1649](https://github.com/googleapis/python-bigquery-dataframes/issues/1649)) ([53caa8d](https://github.com/googleapis/python-bigquery-dataframes/commit/53caa8d689e64436f5313095ee27479a06d8e8a8)) +* Fix `bq_dataframes_template` notebook to work if partial ordering mode is enabled ([#1665](https://github.com/googleapis/python-bigquery-dataframes/issues/1665)) ([f442e7a](https://github.com/googleapis/python-bigquery-dataframes/commit/f442e7a07ff273ba3af74eeabafb62110b78f692)) +* Note that `udf` is in preview and must be python 3.11 compatible ([#1629](https://github.com/googleapis/python-bigquery-dataframes/issues/1629)) ([48d10d1](https://github.com/googleapis/python-bigquery-dataframes/commit/48d10d1f0150a29dd3b91f505f8d3874e0b88c42)) + ## [2.1.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.0.0...v2.1.0) (2025-04-22) diff --git a/bigframes/version.py b/bigframes/version.py index b671169b24..c6ca0ee57c 100644 --- a/bigframes/version.py +++ b/bigframes/version.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.1.0" +__version__ = "2.2.0" # {x-release-please-start-date} -__release_date__ = "2025-04-22" +__release_date__ = "2025-04-30" # {x-release-please-end} diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index b671169b24..c6ca0ee57c 100644 --- a/third_party/bigframes_vendored/version.py +++ b/third_party/bigframes_vendored/version.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.1.0" +__version__ = "2.2.0" # {x-release-please-start-date} -__release_date__ = "2025-04-22" +__release_date__ = "2025-04-30" # {x-release-please-end}