diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6773aef7c2a..cf8fe9cce25 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,6 +36,3 @@ jobs: run: | python -m pip install --upgrade setuptools pip wheel python -m pip install nox - - name: Run docfx - run: | - nox -s docfx diff --git a/.librarian/state.yaml b/.librarian/state.yaml index dc6c05b541d..ebc26d98adf 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,7 +1,7 @@ image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:160860d189ff1c2f7515638478823712fa5b243e27ccc33a2728669fa1e2ed0c libraries: - id: bigframes - version: 2.37.0 + version: 2.38.0 last_generated_commit: "" apis: [] source_roots: diff --git a/CHANGELOG.md b/CHANGELOG.md index b69b87bd451..0fc39e48030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ [1]: https://pypi.org/project/bigframes/#history +## [2.38.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.37.0...v2.38.0) (2026-03-16) + + +### Documentation + +* add notebooks to user guide page (#2505) ([5cf37888bc0b4b1b0993dadd1e0fe5ee08341ef4](https://github.com/googleapis/python-bigquery-dataframes/commit/5cf37888bc0b4b1b0993dadd1e0fe5ee08341ef4)) +* Fix typo in ExperimentOptions class docstring (#2498) ([077cb2ebe515fc5e07bcbb5dc663edd28d3eaf00](https://github.com/googleapis/python-bigquery-dataframes/commit/077cb2ebe515fc5e07bcbb5dc663edd28d3eaf00)) + + +### Features + +* add `df.bigquery` pandas accessor (#2513) ([91b6c245521218bb78b543885e1b9424278ce2ab](https://github.com/googleapis/python-bigquery-dataframes/commit/91b6c245521218bb78b543885e1b9424278ce2ab)) +* use EUC for AI IF, CLASSIFY, and SCORE when connection is not provided (#2507) ([fe94910abff28e244dd79e1540a6c2184a12eb44](https://github.com/googleapis/python-bigquery-dataframes/commit/fe94910abff28e244dd79e1540a6c2184a12eb44)) +* Add `bigframes.bigquery.rand()` function (#2501) ([5c43efb745118f506ecc30196da68e9d6f4346dc](https://github.com/googleapis/python-bigquery-dataframes/commit/5c43efb745118f506ecc30196da68e9d6f4346dc)) +* add bigquery.ml.get_insights function (#2493) ([d29a60953ac989bb2c95e6eec3010620ac776a3c](https://github.com/googleapis/python-bigquery-dataframes/commit/d29a60953ac989bb2c95e6eec3010620ac776a3c)) +* Add str, dt accessors to pd.col Expression objects (#2488) ([ce5de57019449ca77d308946df72f04289343b51](https://github.com/googleapis/python-bigquery-dataframes/commit/ce5de57019449ca77d308946df72f04289343b51)) + + +### Bug Fixes + +* handle unsupported types and empty results in describe (#2506) ([2326ad6aec15c20a66756eff093b50be484b3ba8](https://github.com/googleapis/python-bigquery-dataframes/commit/2326ad6aec15c20a66756eff093b50be484b3ba8)) +* no longer automatically use anywidget in the `%%bqsql` magics (#2504) ([43353e2bc9ffbc38b7383c24ecaac80d3b8bab32](https://github.com/googleapis/python-bigquery-dataframes/commit/43353e2bc9ffbc38b7383c24ecaac80d3b8bab32)) + ## [2.37.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.36.0...v2.37.0) (2026-03-03) diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 4de59125272..00000000000 --- a/GEMINI.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contribution guidelines, tailored for LLM agents - -@.gemini/common/docs.md - -@.gemini/common/constraints.md diff --git a/bigframes/__init__.py b/bigframes/__init__.py index a3a9b4e4c77..9b0d6bb00cc 100644 --- a/bigframes/__init__.py +++ b/bigframes/__init__.py @@ -32,6 +32,9 @@ ) import bigframes.enums as enums # noqa: E402 import bigframes.exceptions as exceptions # noqa: E402 + +# Register pandas extensions +import bigframes.extensions.pandas.dataframe_accessor # noqa: F401, E402 from bigframes.session import connect, Session # noqa: E402 from bigframes.version import __version__ # noqa: E402 diff --git a/bigframes/_config/experiment_options.py b/bigframes/_config/experiment_options.py index 6c51ef6db39..b64c0aaa040 100644 --- a/bigframes/_config/experiment_options.py +++ b/bigframes/_config/experiment_options.py @@ -21,7 +21,7 @@ class ExperimentOptions: """ - Encapsulates the configration for experiments + Encapsulates the configuration for experiments """ def __init__(self): diff --git a/bigframes/_magics.py b/bigframes/_magics.py index 613f71219be..f536108d53d 100644 --- a/bigframes/_magics.py +++ b/bigframes/_magics.py @@ -48,8 +48,4 @@ def _cell_magic(line, cell): if args.destination_var: ipython.push({args.destination_var: dataframe}) - with bigframes.option_context( - "display.repr_mode", - "anywidget", - ): - display(dataframe) + display(dataframe) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index e02e80cd1fb..14e0f315dcc 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -58,6 +58,7 @@ to_json, to_json_string, ) +from bigframes.bigquery._operations.mathematical import rand from bigframes.bigquery._operations.search import create_vector_index, vector_search from bigframes.bigquery._operations.sql import sql_scalar from bigframes.bigquery._operations.struct import struct @@ -99,6 +100,8 @@ parse_json, to_json, to_json_string, + # mathematical ops + rand, # search ops create_vector_index, vector_search, @@ -154,6 +157,8 @@ "parse_json", "to_json", "to_json_string", + # mathematical ops + "rand", # search ops "create_vector_index", "vector_search", diff --git a/bigframes/bigquery/_operations/ai.py b/bigframes/bigquery/_operations/ai.py index dd9c4e236b1..e578f4be4a7 100644 --- a/bigframes/bigquery/_operations/ai.py +++ b/bigframes/bigquery/_operations/ai.py @@ -28,8 +28,9 @@ from bigframes import series, session from bigframes.bigquery._operations import utils as bq_utils from bigframes.core import convert +from bigframes.core.compile.sqlglot import sql as sg_sql from bigframes.core.logging import log_adapter -import bigframes.core.sql.literals +from bigframes.ml import base as ml_base from bigframes.ml import core as ml_core from bigframes.operations import ai_ops, output_schemas @@ -392,7 +393,7 @@ def generate_double( @log_adapter.method_logger(custom_base_name="bigquery_ai") def generate_embedding( - model: Union[bigframes.ml.base.BaseEstimator, str, pd.Series], + model: Union[ml_base.BaseEstimator, str, pd.Series], data: Union[dataframe.DataFrame, series.Series, pd.DataFrame, pd.Series], *, output_dimensionality: Optional[int] = None, @@ -416,7 +417,7 @@ def generate_embedding( ... ) # doctest: +SKIP Args: - model (bigframes.ml.base.BaseEstimator or str): + model (ml_base.BaseEstimator or str): The model to use for text embedding. data (bigframes.pandas.DataFrame or bigframes.pandas.Series): The data to generate embeddings for. If a Series is provided, it is @@ -458,7 +459,7 @@ def generate_embedding( model_name, session = bq_utils.get_model_name_and_session(model, data) table_sql = bq_utils.to_sql(data) - struct_fields: Dict[str, bigframes.core.sql.literals.STRUCT_VALUES] = {} + struct_fields: Dict[str, Any] = {} if output_dimensionality is not None: struct_fields["OUTPUT_DIMENSIONALITY"] = output_dimensionality if task_type is not None: @@ -478,7 +479,7 @@ def generate_embedding( FROM AI.GENERATE_EMBEDDING( MODEL `{model_name}`, ({table_sql}), - {bigframes.core.sql.literals.struct_literal(struct_fields)} + {sg_sql.to_sql(sg_sql.literal(struct_fields))} ) """ @@ -490,7 +491,7 @@ def generate_embedding( @log_adapter.method_logger(custom_base_name="bigquery_ai") def generate_text( - model: Union[bigframes.ml.base.BaseEstimator, str, pd.Series], + model: Union[ml_base.BaseEstimator, str, pd.Series], data: Union[dataframe.DataFrame, series.Series, pd.DataFrame, pd.Series], *, temperature: Optional[float] = None, @@ -519,7 +520,7 @@ def generate_text( ... ) # doctest: +SKIP Args: - model (bigframes.ml.base.BaseEstimator or str): + model (ml_base.BaseEstimator or str): The model to use for text generation. data (bigframes.pandas.DataFrame or bigframes.pandas.Series): The data to generate text for. If a Series is provided, it is @@ -591,7 +592,7 @@ def generate_text( FROM AI.GENERATE_TEXT( MODEL `{model_name}`, ({table_sql}), - {bigframes.core.sql.literals.struct_literal(struct_fields)} + {sg_sql.to_sql(sg_sql.literal(struct_fields))} ) """ @@ -603,7 +604,7 @@ def generate_text( @log_adapter.method_logger(custom_base_name="bigquery_ai") def generate_table( - model: Union[bigframes.ml.base.BaseEstimator, str, pd.Series], + model: Union[ml_base.BaseEstimator, str, pd.Series], data: Union[dataframe.DataFrame, series.Series, pd.DataFrame, pd.Series], *, output_schema: Union[str, Mapping[str, str]], @@ -635,7 +636,7 @@ def generate_table( ... ) # doctest: +SKIP Args: - model (bigframes.ml.base.BaseEstimator or str): + model (ml_base.BaseEstimator or str): The model to use for table generation. data (bigframes.pandas.DataFrame or bigframes.pandas.Series): The data to generate table for. If a Series is provided, it is @@ -677,9 +678,7 @@ def generate_table( else: output_schema_str = output_schema - struct_fields_bq: Dict[str, bigframes.core.sql.literals.STRUCT_VALUES] = { - "output_schema": output_schema_str - } + struct_fields_bq: Dict[str, Any] = {"output_schema": output_schema_str} if temperature is not None: struct_fields_bq["temperature"] = temperature if top_p is not None: @@ -691,7 +690,7 @@ def generate_table( if request_type is not None: struct_fields_bq["request_type"] = request_type - struct_sql = bigframes.core.sql.literals.struct_literal(struct_fields_bq) + struct_sql = sg_sql.to_sql(sg_sql.literal(struct_fields_bq)) query = f""" SELECT * FROM AI.GENERATE_TABLE( @@ -746,7 +745,7 @@ def if_( or pandas Series. connection_id (str, optional): Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`. - If not provided, the connection from the current session will be used. + If not provided, the query uses your end-user credential. Returns: bigframes.series.Series: A new series of bools. @@ -757,7 +756,7 @@ def if_( operator = ai_ops.AIIf( prompt_context=tuple(prompt_context), - connection_id=_resolve_connection_id(series_list[0], connection_id), + connection_id=connection_id, ) return series_list[0]._apply_nary_op(operator, series_list[1:]) @@ -801,7 +800,7 @@ def classify( Categories to classify the input into. connection_id (str, optional): Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`. - If not provided, the connection from the current session will be used. + If not provided, the query uses your end-user credential. Returns: bigframes.series.Series: A new series of strings. @@ -813,7 +812,7 @@ def classify( operator = ai_ops.AIClassify( prompt_context=tuple(prompt_context), categories=tuple(categories), - connection_id=_resolve_connection_id(series_list[0], connection_id), + connection_id=connection_id, ) return series_list[0]._apply_nary_op(operator, series_list[1:]) @@ -854,7 +853,7 @@ def score( or pandas Series. connection_id (str, optional): Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`. - If not provided, the connection from the current session will be used. + If not provided, the query uses your end-user credential. Returns: bigframes.series.Series: A new series of double (float) values. @@ -865,7 +864,7 @@ def score( operator = ai_ops.AIScore( prompt_context=tuple(prompt_context), - connection_id=_resolve_connection_id(series_list[0], connection_id), + connection_id=connection_id, ) return series_list[0]._apply_nary_op(operator, series_list[1:]) diff --git a/bigframes/bigquery/_operations/io.py b/bigframes/bigquery/_operations/io.py index daf28e6aedd..6effbdb2573 100644 --- a/bigframes/bigquery/_operations/io.py +++ b/bigframes/bigquery/_operations/io.py @@ -19,8 +19,8 @@ import pandas as pd from bigframes.bigquery._operations.table import _get_table_metadata +import bigframes.core.compile.sqlglot.sql as sql import bigframes.core.logging.log_adapter as log_adapter -import bigframes.core.sql.io import bigframes.session @@ -73,7 +73,7 @@ def load_data( """ import bigframes.pandas as bpd - sql = bigframes.core.sql.io.load_data_ddl( + load_data_expr = sql.load_data( table_name=table_name, write_disposition=write_disposition, columns=columns, @@ -84,11 +84,12 @@ def load_data( with_partition_columns=with_partition_columns, connection_name=connection_name, ) + sql_text = sql.to_sql(load_data_expr) if session is None: - bpd.read_gbq_query(sql) + bpd.read_gbq_query(sql_text) session = bpd.get_global_session() else: - session.read_gbq_query(sql) + session.read_gbq_query(sql_text) return _get_table_metadata(bqclient=session.bqclient, table_name=table_name) diff --git a/bigframes/bigquery/_operations/mathematical.py b/bigframes/bigquery/_operations/mathematical.py new file mode 100644 index 00000000000..9bd6506981e --- /dev/null +++ b/bigframes/bigquery/_operations/mathematical.py @@ -0,0 +1,53 @@ +# 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 bigframes import dtypes +from bigframes import operations as ops +import bigframes.core.col +import bigframes.core.expression + + +def rand() -> bigframes.core.col.Expression: + """ + Generates a pseudo-random value of type FLOAT64 in the range of [0, 1), + inclusive of 0 and exclusive of 1. + + .. warning:: + This method introduces non-determinism to the expression. Reading the + same column twice may result in different results. The value might + change. Do not use this value or any value derived from it as a join + key. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import bigframes.bigquery as bbq + >>> df = bpd.DataFrame({"a": [1, 2, 3]}) + >>> df['random'] = bbq.rand() + >>> # Resulting column 'random' will contain random floats between 0 and 1. + + Returns: + bigframes.pandas.api.typing.Expression: + An expression that can be used in + :func:`~bigframes.pandas.DataFrame.assign` and other methods. See + :func:`bigframes.pandas.col`. + """ + op = ops.SqlScalarOp( + _output_type=dtypes.FLOAT_DTYPE, + sql_template="RAND()", + is_deterministic=False, + ) + return bigframes.core.col.Expression(bigframes.core.expression.OpExpression(op, ())) diff --git a/bigframes/bigquery/_operations/ml.py b/bigframes/bigquery/_operations/ml.py index d5b1786b258..3e5d6fb263b 100644 --- a/bigframes/bigquery/_operations/ml.py +++ b/bigframes/bigquery/_operations/ml.py @@ -480,6 +480,39 @@ def generate_text( return session.read_gbq_query(sql) +@log_adapter.method_logger(custom_base_name="bigquery_ml") +def get_insights( + model: Union[bigframes.ml.base.BaseEstimator, str, pd.Series], +) -> dataframe.DataFrame: + """ + Gets insights from a BigQuery ML model. + + See the `BigQuery ML GET_INSIGHTS function syntax + `_ + for additional reference. + + Args: + model (bigframes.ml.base.BaseEstimator, str, or pd.Series): + The model to get insights from. + + Returns: + bigframes.pandas.DataFrame: + The insights. + """ + import bigframes.pandas as bpd + + model_name, session = utils.get_model_name_and_session(model) + + sql = bigframes.core.sql.ml.get_insights( + model_name=model_name, + ) + + if session is None: + return bpd.read_gbq_query(sql) + else: + return session.read_gbq_query(sql) + + @log_adapter.method_logger(custom_base_name="bigquery_ml") def generate_embedding( model: Union[bigframes.ml.base.BaseEstimator, str, pd.Series], diff --git a/bigframes/bigquery/_operations/sql.py b/bigframes/bigquery/_operations/sql.py index e6ac1b9c27d..b65dfd2d16e 100644 --- a/bigframes/bigquery/_operations/sql.py +++ b/bigframes/bigquery/_operations/sql.py @@ -16,19 +16,31 @@ from __future__ import annotations -from typing import Sequence +from typing import cast, Optional, Sequence, Union import google.cloud.bigquery -from bigframes.core.compile.sqlglot import sqlglot_ir +from bigframes.core.compile.sqlglot import sql +import bigframes.dataframe import bigframes.dtypes import bigframes.operations import bigframes.series +def _format_names(sql_template: str, dataframe: bigframes.dataframe.DataFrame): + """Turn sql_template from a template that uses names to one that uses + numbers. + """ + names_to_numbers = {name: f"{{{i}}}" for i, name in enumerate(dataframe.columns)} + numbers = [f"{{{i}}}" for i in range(len(dataframe.columns))] + return sql_template.format(*numbers, **names_to_numbers) + + def sql_scalar( sql_template: str, - columns: Sequence[bigframes.series.Series], + columns: Union[bigframes.dataframe.DataFrame, Sequence[bigframes.series.Series]], + *, + output_dtype: Optional[bigframes.dtypes.Dtype] = None, ) -> bigframes.series.Series: """Create a Series from a SQL template. @@ -37,6 +49,9 @@ def sql_scalar( >>> import bigframes.pandas as bpd >>> import bigframes.bigquery as bbq + Either pass in a sequence of series, in which case use integers in the + format strings. + >>> s = bpd.Series(["1.5", "2.5", "3.5"]) >>> s = s.astype(pd.ArrowDtype(pa.decimal128(38, 9))) >>> bbq.sql_scalar("ROUND({0}, 0, 'ROUND_HALF_EVEN')", [s]) @@ -45,13 +60,29 @@ def sql_scalar( 2 4.000000000 dtype: decimal128(38, 9)[pyarrow] + Or pass in a DataFrame, in which case use the column names in the format + strings. + + >>> df = bpd.DataFrame({"a": ["1.5", "2.5", "3.5"]}) + >>> df = df.astype({"a": pd.ArrowDtype(pa.decimal128(38, 9))}) + >>> bbq.sql_scalar("ROUND({a}, 0, 'ROUND_HALF_EVEN')", df) + 0 2.000000000 + 1 2.000000000 + 2 4.000000000 + dtype: decimal128(38, 9)[pyarrow] + Args: sql_template (str): A SQL format string with Python-style {0} placeholders for each of the Series objects in ``columns``. - columns (Sequence[bigframes.pandas.Series]): + columns ( + Sequence[bigframes.pandas.Series] | bigframes.pandas.DataFrame + ): Series objects representing the column inputs to the ``sql_template``. Must contain at least one Series. + output_dtype (a BigQuery DataFrames compatible dtype, optional): + If provided, BigQuery DataFrames uses this to determine the output + of the returned Series. This avoids a dry run query. Returns: bigframes.pandas.Series: @@ -60,31 +91,38 @@ def sql_scalar( Raises: ValueError: If ``columns`` is empty. """ + if isinstance(columns, bigframes.dataframe.DataFrame): + sql_template = _format_names(sql_template, columns) + columns = [ + cast(bigframes.series.Series, columns[column]) for column in columns.columns + ] + if len(columns) == 0: raise ValueError("Must provide at least one column in columns") + base_series = columns[0] + # To integrate this into our expression trees, we need to get the output # type, so we do some manual compilation and a dry run query to get that. # Another benefit of this is that if there is a syntax error in the SQL # template, then this will fail with an error earlier in the process, # aiding users in debugging. - literals_sql = [ - sqlglot_ir._literal(None, column.dtype).sql(dialect="bigquery") - for column in columns - ] - select_sql = sql_template.format(*literals_sql) - dry_run_sql = f"SELECT {select_sql}" - - # Use the executor directly, because we want the original column IDs, not - # the user-friendly column names that block.to_sql_query() would produce. - base_series = columns[0] - bqclient = base_series._session.bqclient - job = bqclient.query( - dry_run_sql, job_config=google.cloud.bigquery.QueryJobConfig(dry_run=True) - ) - _, output_type = bigframes.dtypes.convert_schema_field(job.schema[0]) + if output_dtype is None: + literals_sql = [ + sql.to_sql(sql.literal(None, column.dtype)) for column in columns + ] + select_sql = sql_template.format(*literals_sql) + dry_run_sql = f"SELECT {select_sql}" + + # Use the executor directly, because we want the original column IDs, not + # the user-friendly column names that block.to_sql_query() would produce. + bqclient = base_series._session.bqclient + job = bqclient.query( + dry_run_sql, job_config=google.cloud.bigquery.QueryJobConfig(dry_run=True) + ) + _, output_dtype = bigframes.dtypes.convert_schema_field(job.schema[0]) op = bigframes.operations.SqlScalarOp( - _output_type=output_type, sql_template=sql_template + _output_type=output_dtype, sql_template=sql_template ) return base_series._apply_nary_op(op, columns[1:]) diff --git a/bigframes/bigquery/ml.py b/bigframes/bigquery/ml.py index b1b33d0dbd4..9b0d77d5b89 100644 --- a/bigframes/bigquery/ml.py +++ b/bigframes/bigquery/ml.py @@ -25,6 +25,7 @@ explain_predict, generate_embedding, generate_text, + get_insights, global_explain, predict, transform, @@ -39,4 +40,5 @@ "transform", "generate_text", "generate_embedding", + "get_insights", ] diff --git a/bigframes/core/bigframe_node.py b/bigframes/core/bigframe_node.py index 7e40248a009..c71f2136fc6 100644 --- a/bigframes/core/bigframe_node.py +++ b/bigframes/core/bigframe_node.py @@ -330,20 +330,30 @@ def top_down( """ Perform a top-down transformation of the BigFrameNode tree. """ - to_process = [self] results: Dict[BigFrameNode, BigFrameNode] = {} + # Each stack entry is (node, t_node). t_node is None until transform(node) is called. + stack: list[tuple[BigFrameNode, typing.Optional[BigFrameNode]]] = [(self, None)] - while to_process: - item = to_process.pop() - if item not in results.keys(): - item_result = transform(item) - results[item] = item_result - to_process.extend(item_result.child_nodes) - - to_process = [self] - # for each processed item, replace its children - for item in reversed(list(results.keys())): - results[item] = results[item].transform_children(lambda x: results[x]) + while stack: + node, t_node = stack[-1] + + if t_node is None: + if node in results: + stack.pop() + continue + t_node = transform(node) + stack[-1] = (node, t_node) + + all_done = True + for child in reversed(t_node.child_nodes): + if child not in results: + stack.append((child, None)) + all_done = False + break + + if all_done: + results[node] = t_node.transform_children(lambda x: results[x]) + stack.pop() return results[self] diff --git a/bigframes/core/col.py b/bigframes/core/col.py index d00d61365a9..cad30f8f339 100644 --- a/bigframes/core/col.py +++ b/bigframes/core/col.py @@ -14,7 +14,7 @@ from __future__ import annotations import dataclasses -from typing import Any, Hashable +from typing import Any, Hashable, Literal, TYPE_CHECKING import bigframes_vendored.pandas.core.col as pd_col @@ -23,6 +23,10 @@ import bigframes.operations as bf_ops import bigframes.operations.aggregations as agg_ops +if TYPE_CHECKING: + import bigframes.operations.datetimes as datetimes + import bigframes.operations.strings as strings + # Not to be confused with the Expression class in `bigframes.core.expressions` # Name collision unintended @@ -32,7 +36,7 @@ class Expression: _value: bf_expression.Expression - def _apply_unary(self, op: bf_ops.UnaryOp) -> Expression: + def _apply_unary_op(self, op: bf_ops.UnaryOp) -> Expression: return Expression(op.as_expr(self._value)) def _apply_unary_agg(self, op: agg_ops.UnaryAggregateOp) -> Expression: @@ -44,7 +48,14 @@ def _apply_unary_agg(self, op: agg_ops.UnaryAggregateOp) -> Expression: agg_expressions.WindowExpression(agg_expr, window_spec.unbound()) ) - def _apply_binary(self, other: Any, op: bf_ops.BinaryOp, reverse: bool = False): + # alignment is purely for series compatibility, and is ignored here + def _apply_binary_op( + self, + other: Any, + op: bf_ops.BinaryOp, + alignment: Literal["outer", "left"] = "outer", + reverse: bool = False, + ): if isinstance(other, Expression): other_value = other._value else: @@ -55,79 +66,79 @@ def _apply_binary(self, other: Any, op: bf_ops.BinaryOp, reverse: bool = False): return Expression(op.as_expr(self._value, other_value)) def __add__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.add_op) + return self._apply_binary_op(other, bf_ops.add_op) def __radd__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.add_op, reverse=True) + return self._apply_binary_op(other, bf_ops.add_op, reverse=True) def __sub__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.sub_op) + return self._apply_binary_op(other, bf_ops.sub_op) def __rsub__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.sub_op, reverse=True) + return self._apply_binary_op(other, bf_ops.sub_op, reverse=True) def __mul__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.mul_op) + return self._apply_binary_op(other, bf_ops.mul_op) def __rmul__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.mul_op, reverse=True) + return self._apply_binary_op(other, bf_ops.mul_op, reverse=True) def __truediv__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.div_op) + return self._apply_binary_op(other, bf_ops.div_op) def __rtruediv__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.div_op, reverse=True) + return self._apply_binary_op(other, bf_ops.div_op, reverse=True) def __floordiv__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.floordiv_op) + return self._apply_binary_op(other, bf_ops.floordiv_op) def __rfloordiv__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.floordiv_op, reverse=True) + return self._apply_binary_op(other, bf_ops.floordiv_op, reverse=True) def __ge__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.ge_op) + return self._apply_binary_op(other, bf_ops.ge_op) def __gt__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.gt_op) + return self._apply_binary_op(other, bf_ops.gt_op) def __le__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.le_op) + return self._apply_binary_op(other, bf_ops.le_op) def __lt__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.lt_op) + return self._apply_binary_op(other, bf_ops.lt_op) def __eq__(self, other: object) -> Expression: # type: ignore - return self._apply_binary(other, bf_ops.eq_op) + return self._apply_binary_op(other, bf_ops.eq_op) def __ne__(self, other: object) -> Expression: # type: ignore - return self._apply_binary(other, bf_ops.ne_op) + return self._apply_binary_op(other, bf_ops.ne_op) def __mod__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.mod_op) + return self._apply_binary_op(other, bf_ops.mod_op) def __rmod__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.mod_op, reverse=True) + return self._apply_binary_op(other, bf_ops.mod_op, reverse=True) def __and__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.and_op) + return self._apply_binary_op(other, bf_ops.and_op) def __rand__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.and_op, reverse=True) + return self._apply_binary_op(other, bf_ops.and_op, reverse=True) def __or__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.or_op) + return self._apply_binary_op(other, bf_ops.or_op) def __ror__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.or_op, reverse=True) + return self._apply_binary_op(other, bf_ops.or_op, reverse=True) def __xor__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.xor_op) + return self._apply_binary_op(other, bf_ops.xor_op) def __rxor__(self, other: Any) -> Expression: - return self._apply_binary(other, bf_ops.xor_op, reverse=True) + return self._apply_binary_op(other, bf_ops.xor_op, reverse=True) def __invert__(self) -> Expression: - return self._apply_unary(bf_ops.invert_op) + return self._apply_unary_op(bf_ops.invert_op) def sum(self) -> Expression: return self._apply_unary_agg(agg_ops.sum_op) @@ -147,6 +158,18 @@ def min(self) -> Expression: def max(self) -> Expression: return self._apply_unary_agg(agg_ops.max_op) + @property + def dt(self) -> datetimes.DatetimeSimpleMethods: + import bigframes.operations.datetimes as datetimes + + return datetimes.DatetimeSimpleMethods(self) + + @property + def str(self) -> strings.StringMethods: + import bigframes.operations.strings as strings + + return strings.StringMethods(self) + def col(col_name: Hashable) -> Expression: return Expression(bf_expression.free_var(col_name)) diff --git a/bigframes/core/compile/ibis_compiler/scalar_op_registry.py b/bigframes/core/compile/ibis_compiler/scalar_op_registry.py index 3ae98a267e1..dd275874332 100644 --- a/bigframes/core/compile/ibis_compiler/scalar_op_registry.py +++ b/bigframes/core/compile/ibis_compiler/scalar_op_registry.py @@ -982,7 +982,9 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp): @scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True) def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp): - if x.type() in (ibis_dtypes.str, ibis_dtypes.Timestamp("UTC")): # type: ignore + if x.type() == ibis_dtypes.Timestamp(None): # type: ignore + return x # already a timestamp, no-op + elif x.type() in (ibis_dtypes.str, ibis_dtypes.Timestamp("UTC")): # type: ignore return x.try_cast(ibis_dtypes.Timestamp(None)) # type: ignore else: # Numerical inputs. diff --git a/bigframes/core/compile/sqlglot/aggregations/unary_compiler.py b/bigframes/core/compile/sqlglot/aggregations/unary_compiler.py index add3ccd9231..cca0f021336 100644 --- a/bigframes/core/compile/sqlglot/aggregations/unary_compiler.py +++ b/bigframes/core/compile/sqlglot/aggregations/unary_compiler.py @@ -22,11 +22,11 @@ from bigframes import dtypes from bigframes.core import window_spec +from bigframes.core.compile.sqlglot import sql import bigframes.core.compile.sqlglot.aggregations.op_registration as reg from bigframes.core.compile.sqlglot.aggregations.windows import apply_window_if_present from bigframes.core.compile.sqlglot.expressions import constants import bigframes.core.compile.sqlglot.expressions.typed_expr as typed_expr -import bigframes.core.compile.sqlglot.sqlglot_ir as ir from bigframes.operations import aggregations as agg_ops UNARY_OP_REGISTRATION = reg.OpRegistration() @@ -157,9 +157,9 @@ def _cut_ops_w_int_bins( for this_bin in range(bins): value: sge.Expression if op.labels is False: - value = ir._literal(this_bin, dtypes.INT_DTYPE) + value = sql.literal(this_bin, dtypes.INT_DTYPE) elif isinstance(op.labels, typing.Iterable): - value = ir._literal(list(op.labels)[this_bin], dtypes.STRING_DTYPE) + value = sql.literal(list(op.labels)[this_bin], dtypes.STRING_DTYPE) else: left_adj: sge.Expression = ( adj if this_bin == 0 and op.right else sge.convert(0) @@ -217,10 +217,10 @@ def _cut_ops_w_intervals( ) -> sge.Case: case_expr = sge.Case() for this_bin, interval in enumerate(bins): - left: sge.Expression = ir._literal( + left: sge.Expression = sql.literal( interval[0], dtypes.infer_literal_type(interval[0]) ) - right: sge.Expression = ir._literal( + right: sge.Expression = sql.literal( interval[1], dtypes.infer_literal_type(interval[1]) ) condition: sge.Expression @@ -237,9 +237,9 @@ def _cut_ops_w_intervals( value: sge.Expression if op.labels is False: - value = ir._literal(this_bin, dtypes.INT_DTYPE) + value = sql.literal(this_bin, dtypes.INT_DTYPE) elif isinstance(op.labels, typing.Iterable): - value = ir._literal(list(op.labels)[this_bin], dtypes.STRING_DTYPE) + value = sql.literal(list(op.labels)[this_bin], dtypes.STRING_DTYPE) else: if op.right: left_identifier = sge.Identifier(this="left_exclusive", quoted=True) @@ -609,7 +609,7 @@ def _( # Will be null if all inputs are null. Pandas defaults to zero sum though. zero = pd.to_timedelta(0) if column.dtype == dtypes.TIMEDELTA_DTYPE else 0 - return sge.func("IFNULL", expr, ir._literal(zero, column.dtype)) + return sge.func("IFNULL", expr, sql.literal(zero, column.dtype)) @UNARY_OP_REGISTRATION.register(agg_ops.VarOp) diff --git a/bigframes/core/compile/sqlglot/compiler.py b/bigframes/core/compile/sqlglot/compiler.py index 6b90b94067e..a86a192a9e1 100644 --- a/bigframes/core/compile/sqlglot/compiler.py +++ b/bigframes/core/compile/sqlglot/compiler.py @@ -30,11 +30,11 @@ sql_nodes, ) from bigframes.core.compile import configs +from bigframes.core.compile.sqlglot import sql, sqlglot_ir import bigframes.core.compile.sqlglot.aggregate_compiler as aggregate_compiler from bigframes.core.compile.sqlglot.aggregations import windows import bigframes.core.compile.sqlglot.expression_compiler as expression_compiler from bigframes.core.compile.sqlglot.expressions import typed_expr -import bigframes.core.compile.sqlglot.sqlglot_ir as ir from bigframes.core.logging import data_types as data_type_logger import bigframes.core.ordering as bf_ordering from bigframes.core.rewrite import schema_binding @@ -62,6 +62,8 @@ def compile_sql(request: configs.CompileRequest) -> configs.CompileResult: if request.sort_rows: result_node = typing.cast(nodes.ResultNode, rewrite.column_pruning(result_node)) encoded_type_refs = data_type_logger.encode_type_refs(result_node) + # TODO: Extract CTEs earlier + result_node = typing.cast(nodes.ResultNode, rewrite.extract_ctes(result_node)) sql = _compile_result_node(result_node) return configs.CompileResult( sql, @@ -74,6 +76,8 @@ def compile_sql(request: configs.CompileRequest) -> configs.CompileResult: result_node = dataclasses.replace(result_node, order_by=None) result_node = typing.cast(nodes.ResultNode, rewrite.column_pruning(result_node)) encoded_type_refs = data_type_logger.encode_type_refs(result_node) + # TODO: Extract CTEs earlier + result_node = typing.cast(nodes.ResultNode, rewrite.extract_ctes(result_node)) sql = _compile_result_node(result_node) # Return the ordering iff no extra columns are needed to define the row order if ordering is not None: @@ -94,6 +98,7 @@ def _remap_variables( result_node, _ = rewrite.remap_variables( node, map(identifiers.ColumnId, uid_gen.get_uid_stream("bfcol_")) ) + result_node.validate_tree() return typing.cast(nodes.ResultNode, result_node) @@ -102,26 +107,29 @@ def _compile_result_node(root: nodes.ResultNode) -> str: # of nodes using the same generator. uid_gen = guid.SequentialUIDGenerator() root = _remap_variables(root, uid_gen) + # Remap variables creates too mayn new + # root = rewrite.select_pullup(root, prefer_source_names=False) root = typing.cast(nodes.ResultNode, rewrite.defer_selection(root)) # Have to bind schema as the final step before compilation. # Probably, should defer even further root = typing.cast(nodes.ResultNode, schema_binding.bind_schema_to_tree(root)) - sqlglot_ir = compile_node(rewrite.as_sql_nodes(root), uid_gen) - return sqlglot_ir.sql + # TODO: Bake all IDs in tree, stop passing uid_gen to emitters + sqlglot_ir_obj = compile_node(rewrite.as_sql_nodes(root, uid_gen), uid_gen) + return sqlglot_ir_obj.sql def compile_node( node: nodes.BigFrameNode, uid_gen: guid.SequentialUIDGenerator -) -> ir.SQLGlotIR: +) -> sqlglot_ir.SQLGlotIR: """Compiles the given BigFrameNode from bottem-up into SQLGlotIR.""" - bf_to_sqlglot: dict[nodes.BigFrameNode, ir.SQLGlotIR] = {} - child_results: tuple[ir.SQLGlotIR, ...] = () + bf_to_sqlglot: dict[nodes.BigFrameNode, sqlglot_ir.SQLGlotIR] = {} + child_results: tuple[sqlglot_ir.SQLGlotIR, ...] = () for current_node in list(node.iter_nodes_topo()): if current_node.child_nodes == (): # For leaf node, generates a dumpy child to pass the UID generator. - child_results = tuple([ir.SQLGlotIR(uid_gen=uid_gen)]) + child_results = tuple([sqlglot_ir.SQLGlotIR.empty(uid_gen=uid_gen)]) else: # Child nodes should have been compiled in the reverse topological order. child_results = tuple( @@ -135,14 +143,14 @@ def compile_node( @functools.singledispatch def _compile_node( - node: nodes.BigFrameNode, *compiled_children: ir.SQLGlotIR -) -> ir.SQLGlotIR: + node: nodes.BigFrameNode, *compiled_children: sqlglot_ir.SQLGlotIR +) -> sqlglot_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_sql_select(node: sql_nodes.SqlSelectNode, child: ir.SQLGlotIR): +def compile_sql_select(node: sql_nodes.SqlSelectNode, child: sqlglot_ir.SQLGlotIR): ordering_cols = tuple( sge.Ordered( this=expression_compiler.expression_compiler.compile_expression( @@ -175,7 +183,9 @@ def compile_sql_select(node: sql_nodes.SqlSelectNode, child: ir.SQLGlotIR): @_compile_node.register -def compile_readlocal(node: nodes.ReadLocalNode, child: ir.SQLGlotIR) -> ir.SQLGlotIR: +def compile_readlocal( + node: nodes.ReadLocalNode, child: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: 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.id.sql for item in node.scan_list.items]) @@ -184,16 +194,18 @@ def compile_readlocal(node: nodes.ReadLocalNode, child: ir.SQLGlotIR) -> ir.SQLG if offsets: pa_table = pyarrow_utils.append_offsets(pa_table, offsets) - return ir.SQLGlotIR.from_pyarrow(pa_table, node.schema, uid_gen=child.uid_gen) + return sqlglot_ir.SQLGlotIR.from_pyarrow( + pa_table, node.schema, uid_gen=child.uid_gen + ) @_compile_node.register -def compile_readtable(node: sql_nodes.SqlDataSource, child: ir.SQLGlotIR): - table = node.source.table - return ir.SQLGlotIR.from_table( - table.project_id, - table.dataset_id, - table.table_id, +def compile_readtable(node: sql_nodes.SqlDataSource, child: sqlglot_ir.SQLGlotIR): + table_obj = node.source.table + return sqlglot_ir.SQLGlotIR.from_table( + table_obj.project_id, + table_obj.dataset_id, + table_obj.table_id, uid_gen=child.uid_gen, sql_predicate=node.source.sql_predicate, system_time=node.source.at_time, @@ -202,20 +214,20 @@ def compile_readtable(node: sql_nodes.SqlDataSource, child: ir.SQLGlotIR): @_compile_node.register def compile_join( - node: nodes.JoinNode, left: ir.SQLGlotIR, right: ir.SQLGlotIR -) -> ir.SQLGlotIR: + node: nodes.JoinNode, left: sqlglot_ir.SQLGlotIR, right: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: conditions = tuple( ( typed_expr.TypedExpr( - expression_compiler.expression_compiler.compile_expression(left), - left.output_type, + expression_compiler.expression_compiler.compile_expression(left_expr), + left_expr.output_type, ), typed_expr.TypedExpr( - expression_compiler.expression_compiler.compile_expression(right), - right.output_type, + expression_compiler.expression_compiler.compile_expression(right_expr), + right_expr.output_type, ), ) - for left, right in node.conditions + for left_expr, right_expr in node.conditions ) return left.join( @@ -228,8 +240,8 @@ def compile_join( @_compile_node.register def compile_isin_join( - node: nodes.InNode, left: ir.SQLGlotIR, right: ir.SQLGlotIR -) -> ir.SQLGlotIR: + node: nodes.InNode, left: sqlglot_ir.SQLGlotIR, right: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: right_field = node.right_child.fields[0] conditions = ( typed_expr.TypedExpr( @@ -253,7 +265,26 @@ def compile_isin_join( @_compile_node.register -def compile_concat(node: nodes.ConcatNode, *children: ir.SQLGlotIR) -> ir.SQLGlotIR: +def compile_cte_ref_node(node: sql_nodes.SqlCteRefNode, child: sqlglot_ir.SQLGlotIR): + return sqlglot_ir.SQLGlotIR.from_cte_ref( + node.cte_name, + uid_gen=child.uid_gen, + ) + + +@_compile_node.register +def compile_with_ctes_node( + node: sql_nodes.SqlWithCtesNode, + child: sqlglot_ir.SQLGlotIR, + *ctes: sqlglot_ir.SQLGlotIR, +): + return child.with_ctes(tuple(zip(node.cte_names, ctes))) + + +@_compile_node.register +def compile_concat( + node: nodes.ConcatNode, *children: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: assert len(children) >= 1 uid_gen = children[0].uid_gen @@ -264,15 +295,17 @@ def compile_concat(node: nodes.ConcatNode, *children: ir.SQLGlotIR) -> ir.SQLGlo for default_output_id, output_id in zip(default_output_ids, node.output_ids) ] - return ir.SQLGlotIR.from_union( - [child._as_select() for child in children], + return sqlglot_ir.SQLGlotIR.from_union( + [child.expr.as_select_all() for child in children], output_aliases=output_aliases, uid_gen=uid_gen, ) @_compile_node.register -def compile_explode(node: nodes.ExplodeNode, child: ir.SQLGlotIR) -> ir.SQLGlotIR: +def compile_explode( + node: nodes.ExplodeNode, child: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: offsets_col = node.offsets_col.sql if (node.offsets_col is not None) else None columns = tuple(ref.id.sql for ref in node.column_ids) return child.explode(columns, offsets_col) @@ -280,8 +313,8 @@ def compile_explode(node: nodes.ExplodeNode, child: ir.SQLGlotIR) -> ir.SQLGlotI @_compile_node.register def compile_fromrange( - node: nodes.FromRangeNode, start: ir.SQLGlotIR, end: ir.SQLGlotIR -) -> ir.SQLGlotIR: + node: nodes.FromRangeNode, start: sqlglot_ir.SQLGlotIR, end: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: start_col_id = node.start.fields[0].id end_col_id = node.end.fields[0].id @@ -291,20 +324,22 @@ def compile_fromrange( end_expr = expression_compiler.expression_compiler.compile_expression( expression.DerefOp(end_col_id) ) - step_expr = ir._literal(node.step, dtypes.INT_DTYPE) + step_expr = sql.literal(node.step, dtypes.INT_DTYPE) return start.resample(end, node.output_id.sql, start_expr, end_expr, step_expr) @_compile_node.register def compile_random_sample( - node: nodes.RandomSampleNode, child: ir.SQLGlotIR -) -> ir.SQLGlotIR: + node: nodes.RandomSampleNode, child: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: return child.sample(node.fraction) @_compile_node.register -def compile_aggregate(node: nodes.AggregateNode, child: ir.SQLGlotIR) -> ir.SQLGlotIR: +def compile_aggregate( + node: nodes.AggregateNode, child: sqlglot_ir.SQLGlotIR +) -> sqlglot_ir.SQLGlotIR: # The BigQuery ordered aggregation cannot support for NULL FIRST/LAST, # so we need to add extra expressions to enforce the null ordering. ordering_cols = windows.get_window_order_by(node.order_by, override_null_order=True) diff --git a/bigframes/core/compile/sqlglot/expression_compiler.py b/bigframes/core/compile/sqlglot/expression_compiler.py index b2ff34bf747..49780fbaea5 100644 --- a/bigframes/core/compile/sqlglot/expression_compiler.py +++ b/bigframes/core/compile/sqlglot/expression_compiler.py @@ -19,8 +19,8 @@ import bigframes_vendored.sqlglot.expressions as sge import bigframes.core.agg_expressions as agg_exprs +from bigframes.core.compile.sqlglot import sql from bigframes.core.compile.sqlglot.expressions.typed_expr import TypedExpr -import bigframes.core.compile.sqlglot.sqlglot_ir as ir import bigframes.core.expression as ex import bigframes.operations as ops @@ -77,7 +77,7 @@ def _(self, expr: ex.DerefOp) -> sge.Expression: @compile_expression.register def _(self, expr: ex.ScalarConstantExpression) -> sge.Expression: - return ir._literal(expr.value, expr.dtype) + return sql.literal(expr.value, expr.dtype) @compile_expression.register def _(self, expr: agg_exprs.WindowExpression) -> sge.Expression: diff --git a/bigframes/core/compile/sqlglot/expressions/ai_ops.py b/bigframes/core/compile/sqlglot/expressions/ai_ops.py index cc0cbaad8fe..df659097b33 100644 --- a/bigframes/core/compile/sqlglot/expressions/ai_ops.py +++ b/bigframes/core/compile/sqlglot/expressions/ai_ops.py @@ -113,9 +113,9 @@ def _construct_named_args(op: ops.NaryOp) -> list[sge.Kwarg]: ) ) - endpoit = op_args.get("endpoint", None) - if endpoit is not None: - args.append(sge.Kwarg(this="endpoint", expression=sge.Literal.string(endpoit))) + endpoint = op_args.get("endpoint", None) + if endpoint is not None: + args.append(sge.Kwarg(this="endpoint", expression=sge.Literal.string(endpoint))) request_type = op_args.get("request_type", None) if request_type is not None: diff --git a/bigframes/core/compile/sqlglot/expressions/comparison_ops.py b/bigframes/core/compile/sqlglot/expressions/comparison_ops.py index f767314be74..7177f9de84b 100644 --- a/bigframes/core/compile/sqlglot/expressions/comparison_ops.py +++ b/bigframes/core/compile/sqlglot/expressions/comparison_ops.py @@ -22,7 +22,7 @@ from bigframes import dtypes from bigframes import operations as ops -from bigframes.core.compile.sqlglot import sqlglot_ir +from bigframes.core.compile.sqlglot import sql import bigframes.core.compile.sqlglot.expression_compiler as expression_compiler from bigframes.core.compile.sqlglot.expressions.typed_expr import TypedExpr @@ -33,35 +33,47 @@ @register_unary_op(ops.IsInOp, pass_op=True) def _(expr: TypedExpr, op: ops.IsInOp) -> sge.Expression: values = [] + # bools are not comparable to non-bools in SQL, so we need to cast the expression to INT64 if the values contain non-bools. + must_upcast_bools = dtypes.is_numeric(expr.dtype, include_bool=False) or any( + dtypes.is_numeric(dtypes.bigframes_type(type(value)), include_bool=False) + for value in op.values + if not _is_null(value) + ) for value in op.values: if _is_null(value): continue dtype = dtypes.bigframes_type(type(value)) if dtypes.can_compare(expr.dtype, dtype): + if must_upcast_bools and dtype == dtypes.BOOL_DTYPE: + value = int(value) values.append(sge.convert(value)) + sg_lexpr: sge.Expression = expr.expr + if expr.dtype == dtypes.BOOL_DTYPE and must_upcast_bools: + sg_lexpr = sge.cast(expr.expr, "INT64") + if op.match_nulls: contains_nulls = any(_is_null(value) for value in op.values) if contains_nulls: if len(values) == 0: - return sge.Is(this=expr.expr, expression=sge.Null()) - return sge.Is(this=expr.expr, expression=sge.Null()) | sge.In( - this=expr.expr, expressions=values + return sge.Is(this=sg_lexpr, expression=sge.Null()) + return sge.Is(this=sg_lexpr, expression=sge.Null()) | sge.In( + this=sg_lexpr, expressions=values ) if len(values) == 0: return sge.convert(False) return sge.func( - "COALESCE", sge.In(this=expr.expr, expressions=values), sge.convert(False) + "COALESCE", sge.In(this=sg_lexpr, expressions=values), sge.convert(False) ) @register_binary_op(ops.eq_op) def _(left: TypedExpr, right: TypedExpr) -> sge.Expression: - if sqlglot_ir._is_null_literal(left.expr): + if sql.is_null_literal(left.expr): return sge.Is(this=right.expr, expression=sge.Null()) - if sqlglot_ir._is_null_literal(right.expr): + if sql.is_null_literal(right.expr): return sge.Is(this=left.expr, expression=sge.Null()) left_expr = _coerce_bool_to_int(left) right_expr = _coerce_bool_to_int(right) @@ -140,12 +152,12 @@ def _(left: TypedExpr, right: TypedExpr) -> sge.Expression: @register_binary_op(ops.ne_op) def _(left: TypedExpr, right: TypedExpr) -> sge.Expression: - if sqlglot_ir._is_null_literal(left.expr): + if sql.is_null_literal(left.expr): return sge.Is( this=sge.paren(right.expr, copy=False), expression=sg.not_(sge.Null(), copy=False), ) - if sqlglot_ir._is_null_literal(right.expr): + if sql.is_null_literal(right.expr): return sge.Is( this=sge.paren(left.expr, copy=False), expression=sg.not_(sge.Null(), copy=False), diff --git a/bigframes/core/compile/sqlglot/expressions/datetime_ops.py b/bigframes/core/compile/sqlglot/expressions/datetime_ops.py index a1c70262d55..21f8b39e7d6 100644 --- a/bigframes/core/compile/sqlglot/expressions/datetime_ops.py +++ b/bigframes/core/compile/sqlglot/expressions/datetime_ops.py @@ -371,7 +371,12 @@ def _(expr: TypedExpr, op: ops.ToDatetimeOp) -> sge.Expression: ) return sge.Cast(this=result, to="DATETIME") - if expr.dtype in (dtypes.STRING_DTYPE, dtypes.TIMESTAMP_DTYPE): + if expr.dtype in ( + dtypes.STRING_DTYPE, + dtypes.TIMESTAMP_DTYPE, + dtypes.DATETIME_DTYPE, + dtypes.DATE_DTYPE, + ): return sge.TryCast(this=expr.expr, to="DATETIME") value = expr.expr @@ -396,7 +401,12 @@ def _(expr: TypedExpr, op: ops.ToTimestampOp) -> sge.Expression: "PARSE_TIMESTAMP", sge.convert(op.format), expr.expr, sge.convert("UTC") ) - if expr.dtype in (dtypes.STRING_DTYPE, dtypes.DATETIME_DTYPE): + if expr.dtype in ( + dtypes.STRING_DTYPE, + dtypes.DATETIME_DTYPE, + dtypes.TIMESTAMP_DTYPE, + dtypes.DATE_DTYPE, + ): return sge.func("TIMESTAMP", expr.expr) value = expr.expr diff --git a/bigframes/core/compile/sqlglot/expressions/generic_ops.py b/bigframes/core/compile/sqlglot/expressions/generic_ops.py index 14af91e591b..46032145e22 100644 --- a/bigframes/core/compile/sqlglot/expressions/generic_ops.py +++ b/bigframes/core/compile/sqlglot/expressions/generic_ops.py @@ -19,7 +19,7 @@ from bigframes import dtypes from bigframes import operations as ops -from bigframes.core.compile.sqlglot import sqlglot_ir, sqlglot_types +from bigframes.core.compile.sqlglot import sql, sqlglot_types import bigframes.core.compile.sqlglot.expression_compiler as expression_compiler from bigframes.core.compile.sqlglot.expressions.typed_expr import TypedExpr @@ -48,8 +48,8 @@ def _(expr: TypedExpr, op: ops.AsTypeOp) -> sge.Expression: return result if to_type == dtypes.FLOAT_DTYPE and from_type == dtypes.BOOL_DTYPE: - sg_expr = _cast(sg_expr, "INT64", op.safe) - return _cast(sg_expr, sg_to_type, op.safe) + sg_expr = sql.cast(sg_expr, "INT64", op.safe) + return sql.cast(sg_expr, sg_to_type, op.safe) if to_type == dtypes.BOOL_DTYPE: if from_type == dtypes.BOOL_DTYPE: @@ -58,16 +58,16 @@ def _(expr: TypedExpr, op: ops.AsTypeOp) -> sge.Expression: return sge.NEQ(this=sg_expr, expression=sge.convert(0)) if to_type == dtypes.STRING_DTYPE: - sg_expr = _cast(sg_expr, sg_to_type, op.safe) + sg_expr = sql.cast(sg_expr, sg_to_type, op.safe) if from_type == dtypes.BOOL_DTYPE: sg_expr = sge.func("INITCAP", sg_expr) return sg_expr if dtypes.is_time_like(to_type) and from_type == dtypes.INT_DTYPE: sg_expr = sge.func("TIMESTAMP_MICROS", sg_expr) - return _cast(sg_expr, sg_to_type, op.safe) + return sql.cast(sg_expr, sg_to_type, op.safe) - return _cast(sg_expr, sg_to_type, op.safe) + return sql.cast(sg_expr, sg_to_type, op.safe) @register_unary_op(ops.hash_op) @@ -104,17 +104,19 @@ def _(expr: TypedExpr, op: ops.MapOp) -> sge.Expression: mappings = [ ( - sqlglot_ir._literal(key, dtypes.is_compatible(key, expr.dtype)), - sqlglot_ir._literal(value, dtypes.is_compatible(value, expr.dtype)), + sql.literal(key, dtypes.is_compatible(key, expr.dtype)), + sql.literal(value, dtypes.is_compatible(value, expr.dtype)), ) for key, value in op.mappings ] return sge.Case( ifs=[ sge.If( - this=sge.EQ(this=expr.expr, expression=key) - if not sqlglot_ir._is_null_literal(key) - else sge.Is(this=expr.expr, expression=sge.Null()), + this=( + sge.EQ(this=expr.expr, expression=key) + if not sql.is_null_literal(key) + else sge.Is(this=expr.expr, expression=sge.Null()) + ), true=value, ) for key, value in mappings @@ -201,12 +203,14 @@ def _(*cases_and_outputs: TypedExpr) -> sge.Expression: ) if do_upcast_bool: result_values = tuple( - TypedExpr( - sge.Cast(this=val.expr, to="INT64"), - dtypes.INT_DTYPE, + ( + TypedExpr( + sge.Cast(this=val.expr, to="INT64"), + dtypes.INT_DTYPE, + ) + if val.dtype == dtypes.BOOL_DTYPE + else val ) - if val.dtype == dtypes.BOOL_DTYPE - else val for val in result_values ) @@ -286,30 +290,23 @@ def _cast_to_int(expr: TypedExpr, op: ops.AsTypeOp) -> sge.Expression | None: sg_expr = expr.expr # Cannot cast DATETIME to INT directly so need to convert to TIMESTAMP first. if from_type == dtypes.DATETIME_DTYPE: - sg_expr = _cast(sg_expr, "TIMESTAMP", op.safe) + sg_expr = sql.cast(sg_expr, "TIMESTAMP", op.safe) return sge.func("UNIX_MICROS", sg_expr) if from_type == dtypes.TIMESTAMP_DTYPE: return sge.func("UNIX_MICROS", sg_expr) if from_type == dtypes.TIME_DTYPE: return sge.func( "TIME_DIFF", - _cast(sg_expr, "TIME", op.safe), + sql.cast(sg_expr, "TIME", op.safe), sge.convert("00:00:00"), "MICROSECOND", ) if from_type == dtypes.NUMERIC_DTYPE or from_type == dtypes.FLOAT_DTYPE: sg_expr = sge.func("TRUNC", sg_expr) - return _cast(sg_expr, "INT64", op.safe) + return sql.cast(sg_expr, "INT64", op.safe) return None -def _cast(expr: sge.Expression, to: str, safe: bool): - if safe: - return sge.TryCast(this=expr, to=to) - else: - return sge.Cast(this=expr, to=to) - - def _convert_to_nonnull_string_sqlglot(expr: TypedExpr) -> sge.Expression: col_type = expr.dtype sg_expr = expr.expr diff --git a/bigframes/core/compile/sqlglot/sql/__init__.py b/bigframes/core/compile/sqlglot/sql/__init__.py new file mode 100644 index 00000000000..17c78ba379a --- /dev/null +++ b/bigframes/core/compile/sqlglot/sql/__init__.py @@ -0,0 +1,42 @@ +# Copyright 2026 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 bigframes.core.compile.sqlglot.sql.base import ( + cast, + escape_chars, + identifier, + is_null_literal, + literal, + table, + to_sql, +) +from bigframes.core.compile.sqlglot.sql.ddl import load_data +from bigframes.core.compile.sqlglot.sql.dml import insert, replace + +__all__ = [ + # From base.py + "cast", + "escape_chars", + "identifier", + "is_null_literal", + "literal", + "table", + "to_sql", + # From ddl.py + "load_data", + # From dml.py + "insert", + "replace", +] diff --git a/bigframes/core/compile/sqlglot/sql/base.py b/bigframes/core/compile/sqlglot/sql/base.py new file mode 100644 index 00000000000..6e888fdf5e8 --- /dev/null +++ b/bigframes/core/compile/sqlglot/sql/base.py @@ -0,0 +1,168 @@ +# Copyright 2026 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 typing + +import bigframes_vendored.sqlglot as sg +import bigframes_vendored.sqlglot.expressions as sge +from google.cloud import bigquery +import numpy as np +import pandas as pd +import pyarrow as pa + +from bigframes import dtypes +from bigframes.core import utils +from bigframes.core.compile.sqlglot.expressions import constants +import bigframes.core.compile.sqlglot.sqlglot_types as sgt + +# 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 + + +QUOTED: bool = True +"""Whether to quote identifiers in the generated SQL.""" + +PRETTY: bool = True +"""Whether to pretty-print the generated SQL.""" + +DIALECT = sg.dialects.bigquery.BigQuery +"""The SQL dialect used for generation.""" + + +def to_sql(expr: sge.Expression) -> str: + """Generate SQL string from the given expression.""" + return expr.sql(dialect=DIALECT, pretty=PRETTY) + + +def identifier(id: str) -> sge.Identifier: + """Return a string representing column reference in a SQL.""" + return sge.to_identifier(id, quoted=QUOTED) + + +def literal(value: typing.Any, dtype: dtypes.Dtype | None = None) -> sge.Expression: + """Return a string representing column reference in a SQL.""" + if dtype is None: + dtype = dtypes.infer_literal_type(value) + + sqlglot_type = sgt.from_bigframes_dtype(dtype) if dtype else None + if sqlglot_type is None: + if not pd.isna(value): + raise ValueError(f"Cannot infer SQLGlot type from None dtype: {value}") + return sge.Null() + + if value is None: + return cast(sge.Null(), sqlglot_type) + if 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) + elif dtype == dtypes.FLOAT_DTYPE: + if pd.isna(value): + if isinstance(value, (float, np.floating)) and np.isnan(value): + return constants._NAN + return cast(sge.Null(), sqlglot_type) + if np.isinf(value): + return constants._INF if value > 0 else constants._NEG_INF + return sge.convert(value) + elif pd.isna(value) or (isinstance(value, pa.Scalar) and not value.is_valid): + return cast(sge.Null(), sqlglot_type) + elif dtype == dtypes.JSON_DTYPE: + return sge.ParseJSON(this=sge.convert(str(value))) + elif dtype == dtypes.BYTES_DTYPE: + return cast(str(value), sqlglot_type) + elif dtypes.is_time_like(dtype): + if isinstance(value, str): + return cast(sge.convert(value), sqlglot_type) + if isinstance(value, np.generic): + value = value.item() + return cast(sge.convert(value.isoformat()), sqlglot_type) + elif dtype in (dtypes.NUMERIC_DTYPE, dtypes.BIGNUMERIC_DTYPE): + return cast(sge.convert(value), 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.TIMEDELTA_DTYPE: + return sge.convert(utils.timedelta_to_micros(value)) + else: + if isinstance(value, np.generic): + value = value.item() + if isinstance(value, pa.Scalar): + value = value.as_py() + return sge.convert(value) + + +def cast(arg: typing.Any, to: str, safe: bool = False) -> sge.Cast | sge.TryCast: + """Return a SQL expression that casts the given argument to the specified type.""" + if safe: + return sge.TryCast(this=arg, to=to) + else: + return sge.Cast(this=arg, to=to) + + +def table(table: bigquery.TableReference) -> sge.Table: + """Return a SQLGlot Table expression representing the given BigQuery table reference.""" + return sge.Table( + this=sge.to_identifier(table.table_id, quoted=True), + db=sge.to_identifier(table.dataset_id, quoted=True), + catalog=sge.to_identifier(table.project, quoted=True), + ) + + +def escape_chars(value: str): + """Escapes all special characters""" + # TODO: Reuse literal's escaping logic instead of re-implementing it here. + # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals + trans_table = str.maketrans( + { + "\a": r"\a", + "\b": r"\b", + "\f": r"\f", + "\n": r"\n", + "\r": r"\r", + "\t": r"\t", + "\v": r"\v", + "\\": r"\\", + "?": r"\?", + '"': r"\"", + "'": r"\'", + "`": r"\`", + } + ) + return value.translate(trans_table) + + +def is_null_literal(expr: sge.Expression) -> bool: + """Checks if the given expression is a NULL literal.""" + if isinstance(expr, sge.Null): + return True + if isinstance(expr, sge.Cast) and isinstance(expr.this, sge.Null): + return True + return False diff --git a/bigframes/core/compile/sqlglot/sql/ddl.py b/bigframes/core/compile/sqlglot/sql/ddl.py new file mode 100644 index 00000000000..911c63781b0 --- /dev/null +++ b/bigframes/core/compile/sqlglot/sql/ddl.py @@ -0,0 +1,164 @@ +# Copyright 2026 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 Mapping, Optional, Union + +import bigframes_vendored.sqlglot as sg +import bigframes_vendored.sqlglot.expressions as sge + +from bigframes.core.compile.sqlglot.sql import base + + +def _loaddata_sql(self: sg.Generator, expression: sge.LoadData) -> str: + out = ["LOAD DATA"] + if expression.args.get("overwrite"): + out.append("OVERWRITE") + + out.append(f"INTO {self.sql(expression, 'this').strip()}") + + # We ignore inpath as it's just a dummy to satisfy sqlglot requirements + # but BigQuery uses FROM FILES instead. + + columns = self.sql(expression, "columns").strip() + if columns: + out.append(columns) + + partition_by = self.sql(expression, "partition_by").strip() + if partition_by: + out.append(partition_by) + + cluster_by = self.sql(expression, "cluster_by").strip() + if cluster_by: + out.append(cluster_by) + + options = self.sql(expression, "options").strip() + if options: + out.append(options) + + from_files = self.sql(expression, "from_files").strip() + if from_files: + out.append(f"FROM FILES {from_files}") + + with_partition_columns = self.sql(expression, "with_partition_columns").strip() + if with_partition_columns: + out.append(f"WITH PARTITION COLUMNS {with_partition_columns}") + + connection = self.sql(expression, "connection").strip() + if connection: + out.append(f"WITH CONNECTION {connection}") + + return " ".join(out) + + +# Register the transform for BigQuery generator +sg.dialects.bigquery.BigQuery.Generator.TRANSFORMS[sge.LoadData] = _loaddata_sql + + +def load_data( + table_name: str, + *, + write_disposition: str = "INTO", + columns: Optional[Mapping[str, str]] = None, + partition_by: Optional[list[str]] = None, + cluster_by: Optional[list[str]] = None, + table_options: Optional[Mapping[str, Union[str, int, float, bool, list]]] = None, + from_files_options: Mapping[str, Union[str, int, float, bool, list]], + with_partition_columns: Optional[Mapping[str, str]] = None, + connection_name: Optional[str] = None, +) -> sge.LoadData: + """Generates the LOAD DATA DDL statement.""" + # We use a Table with a simple identifier for the table name. + # Quoting is handled by the dialect. + table_expr = sge.Table(this=base.identifier(table_name)) + + sge_columns = ( + sge.Schema( + this=None, + expressions=[ + sge.ColumnDef( + this=base.identifier(name), + kind=sge.DataType.build(typ, dialect="bigquery"), + ) + for name, typ in columns.items() + ], + ) + if columns + else None + ) + + sge_partition_by = ( + sge.PartitionedByProperty( + this=base.identifier(partition_by[0]) + if len(partition_by) == 1 + else sge.Tuple(expressions=[base.identifier(col) for col in partition_by]) + ) + if partition_by + else None + ) + + sge_cluster_by = ( + sge.Cluster(expressions=[base.identifier(col) for col in cluster_by]) + if cluster_by + else None + ) + + sge_table_options = ( + sge.Properties( + expressions=[ + sge.Property(this=base.identifier(k), value=base.literal(v)) + for k, v in table_options.items() + ] + ) + if table_options + else None + ) + + sge_from_files = sge.Tuple( + expressions=[ + sge.Property(this=base.identifier(k), value=base.literal(v)) + for k, v in from_files_options.items() + ] + ) + + sge_with_partition_columns = ( + sge.Schema( + this=None, + expressions=[ + sge.ColumnDef( + this=base.identifier(name), + kind=sge.DataType.build(typ, dialect="bigquery"), + ) + for name, typ in with_partition_columns.items() + ], + ) + if with_partition_columns + else None + ) + + sge_connection = base.identifier(connection_name) if connection_name else None + + return sge.LoadData( + this=table_expr, + overwrite=(write_disposition == "OVERWRITE"), + inpath=sge.convert("fake"), # satisfy sqlglot's required inpath arg + columns=sge_columns, + partition_by=sge_partition_by, + cluster_by=sge_cluster_by, + options=sge_table_options, + from_files=sge_from_files, + with_partition_columns=sge_with_partition_columns, + connection=sge_connection, + ) diff --git a/bigframes/core/compile/sqlglot/sql/dml.py b/bigframes/core/compile/sqlglot/sql/dml.py new file mode 100644 index 00000000000..1b21518e74d --- /dev/null +++ b/bigframes/core/compile/sqlglot/sql/dml.py @@ -0,0 +1,59 @@ +# Copyright 2026 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 typing + +import bigframes_vendored.sqlglot.expressions as sge +from google.cloud import bigquery + +from bigframes import dtypes +from bigframes.core.compile.sqlglot.sql import base + + +def insert( + query_or_table: typing.Union[sge.Select, sge.Table], + destination: bigquery.TableReference, +) -> sge.Insert: + """Generates an INSERT INTO SQL statement from the given SELECT statement or + table reference.""" + return sge.insert(_as_from_item(query_or_table), base.table(destination)) + + +def replace( + query_or_table: typing.Union[sge.Select, sge.Table], + destination: bigquery.TableReference, +) -> sge.Merge: + """Generates a MERGE statement to replace the contents of the destination table.""" + return sge.Merge( + this=base.table(destination), + using=_as_from_item(query_or_table), + on=base.literal(False, dtypes.BOOL_DTYPE), + whens=sge.Whens( + expressions=[ + sge.When(matched=False, source=True, then=sge.Delete()), + sge.When(matched=False, then=sge.Insert(this=sge.Var(this="ROW"))), + ] + ), + ) + + +def _as_from_item( + query_or_table: typing.Union[sge.Select, sge.Table] +) -> typing.Union[sge.Subquery, sge.Table]: + if isinstance(query_or_table, sge.Select): + return query_or_table.subquery() + else: # table + return query_or_table diff --git a/bigframes/core/compile/sqlglot/sqlglot_ir.py b/bigframes/core/compile/sqlglot/sqlglot_ir.py index 94ffa39dae5..8a4413fb8b5 100644 --- a/bigframes/core/compile/sqlglot/sqlglot_ir.py +++ b/bigframes/core/compile/sqlglot/sqlglot_ir.py @@ -21,14 +21,12 @@ import bigframes_vendored.sqlglot as sg import bigframes_vendored.sqlglot.expressions as sge -from google.cloud import bigquery -import numpy as np -import pandas as pd import pyarrow as pa from bigframes import dtypes -from bigframes.core import guid, local_data, schema, utils -from bigframes.core.compile.sqlglot.expressions import constants, typed_expr +from bigframes.core import guid, local_data, schema +from bigframes.core.compile.sqlglot import sql +from bigframes.core.compile.sqlglot.expressions import typed_expr import bigframes.core.compile.sqlglot.sqlglot_types as sgt # shapely.wkt.dumps was moved to shapely.io.to_wkt in 2.0. @@ -40,29 +38,92 @@ to_wkt = dumps +class SelectFragment: + def __init__(self, select_expr: sge.Select): + self.select_expr = select_expr + + def as_select_all(self) -> sge.Select: + return self.select_expr + + def select(self, *items: sge.Expression) -> sge.Select: + return sge.Select().select(*items).from_(self.select_expr.subquery()) + + def as_from_item(self) -> sge.Expression: + return self.select_expr.subquery() + + +class TableFragment: + def __init__(self, table: sge.Table | sge.Unnest): + self.table = table + + def as_select_all(self) -> sge.Select: + return sge.Select().select(sge.Star()).from_(self.table) + + def select(self, *items: sge.Expression) -> sge.Select: + return sge.Select().select(*items).from_(self.table) + + def as_from_item(self) -> sge.Expression: + return self.table + + +class DeferredSelectFragment: + def __init__(self, select_supplier: typing.Callable[[sge.Select], sge.Select]): + self.select_supplier = select_supplier + + def as_select_all(self) -> sge.Select: + return self.select_supplier(sge.Select().select(sge.Star())) + + def select(self, *items: sge.Expression) -> sge.Select: + return self.select_supplier(sge.Select().select(*items)) + + def as_from_item(self) -> sge.Expression: + return self.select_supplier(sge.Select().select(sge.Star())).subquery() + + +ExprT = SelectFragment | TableFragment | DeferredSelectFragment + + @dataclasses.dataclass(frozen=True) class SQLGlotIR: """Helper class to build SQLGlot Query and generate SQL string.""" - expr: typing.Union[sge.Select, sge.Table] = sg.select() + expr: ExprT """The SQLGlot expression representing the query.""" - dialect = sg.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.""" - uid_gen: guid.SequentialUIDGenerator = guid.SequentialUIDGenerator() """Generator for unique identifiers.""" @property def sql(self) -> str: """Generate SQL string from the given expression.""" - return self.expr.sql(dialect=self.dialect, pretty=self.pretty) + return sql.to_sql(self.expr.as_select_all()) + + @classmethod + def empty( + cls, uid_gen: guid.SequentialUIDGenerator = guid.SequentialUIDGenerator() + ) -> SQLGlotIR: + return cls(expr=SelectFragment(sge.select()), uid_gen=uid_gen) + + @classmethod + def from_expr( + cls, + expr: sge.Expression, + uid_gen: guid.SequentialUIDGenerator = guid.SequentialUIDGenerator(), + ) -> SQLGlotIR: + if isinstance(expr, sge.Select): + return cls(expr=SelectFragment(expr), uid_gen=uid_gen) + elif isinstance(expr, (sge.Table, sge.Unnest)): + return cls(expr=TableFragment(expr), uid_gen=uid_gen) + else: + raise ValueError(f"Unsupported expression type: {type(expr)}") + + @classmethod + def from_func( + cls, + select_handler: typing.Callable[[sge.Select], sge.Select], + uid_gen: guid.SequentialUIDGenerator = guid.SequentialUIDGenerator(), + ): + return cls(expr=DeferredSelectFragment(select_handler), uid_gen=uid_gen) @classmethod def from_pyarrow( @@ -89,7 +150,7 @@ def from_pyarrow( data_expr = [ sge.Struct( expressions=tuple( - _literal( + sql.literal( value=value, dtype=field.dtype, ) @@ -108,7 +169,7 @@ def from_pyarrow( ), ], ) - return cls(expr=sg.select(sge.Star()).from_(expr), uid_gen=uid_gen) + return cls.from_expr(expr=expr, uid_gen=uid_gen) @classmethod def from_table( @@ -143,20 +204,31 @@ def from_table( ) table_alias = next(uid_gen.get_uid_stream("bft_")) table_expr = sge.Table( - this=sg.to_identifier(table_id, quoted=cls.quoted), - db=sg.to_identifier(dataset_id, quoted=cls.quoted), - catalog=sg.to_identifier(project_id, quoted=cls.quoted), + this=sql.identifier(table_id), + db=sql.identifier(dataset_id), + catalog=sql.identifier(project_id), version=version, - alias=sge.Identifier(this=table_alias, quoted=cls.quoted), + alias=sql.identifier(table_alias), ) if sql_predicate: select_expr = sge.Select().select(sge.Star()).from_(table_expr) select_expr = select_expr.where( - sg.parse_one(sql_predicate, dialect=cls.dialect), append=False + sg.parse_one(sql_predicate, dialect=sql.base.DIALECT), append=False ) - return cls(expr=select_expr, uid_gen=uid_gen) + return cls.from_expr(expr=select_expr, uid_gen=uid_gen) - return cls(expr=table_expr, uid_gen=uid_gen) + return cls.from_expr(expr=table_expr, uid_gen=uid_gen) + + @classmethod + def from_cte_ref( + cls, + cte_ref: str, + uid_gen: guid.SequentialUIDGenerator, + ) -> SQLGlotIR: + table_expr = sge.Table( + this=sql.identifier(cte_ref), + ) + return cls.from_expr(expr=table_expr, uid_gen=uid_gen) def select( self, @@ -166,27 +238,22 @@ def select( limit: typing.Optional[int] = None, ) -> SQLGlotIR: # TODO: Explicitly insert CTEs into plan - if isinstance(self.expr, sge.Select): - new_expr, _ = self._select_to_cte() - else: - new_expr = sge.Select().from_(self.expr) - - if len(sorting) > 0: - new_expr = new_expr.order_by(*sorting) - if len(selections) > 0: to_select = [ sge.Alias( this=expr, - alias=sge.to_identifier(id, quoted=self.quoted), + alias=sql.identifier(id), ) if expr.alias_or_name != id else expr for id, expr in selections ] - new_expr = new_expr.select(*to_select, append=False) + new_expr = self.expr.select(*to_select) else: - new_expr = new_expr.select(sge.Star(), append=False) + new_expr = self.expr.as_select_all() + + if len(sorting) > 0: + new_expr = new_expr.order_by(*sorting) if len(predicates) > 0: condition = _and(predicates) @@ -194,10 +261,10 @@ def select( if limit is not None: new_expr = new_expr.limit(limit) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) @classmethod - def from_query_string( + def from_unparsed_query( cls, query_string: str, ) -> SQLGlotIR: @@ -205,16 +272,14 @@ def from_query_string( in a CTE can avoid the query parsing issue for unsupported syntax in SQLGlot.""" uid_gen: guid.SequentialUIDGenerator = guid.SequentialUIDGenerator() - cte_name = sge.to_identifier( - next(uid_gen.get_uid_stream("bfcte_")), quoted=cls.quoted - ) + cte_name = sql.identifier(next(uid_gen.get_uid_stream("bfcte_"))) cte = sge.CTE( this=query_string, alias=cte_name, ) select_expr = sge.Select().select(sge.Star()).from_(sge.Table(this=cte_name)) select_expr = _set_query_ctes(select_expr, [cte]) - return cls(expr=select_expr, uid_gen=uid_gen) + return cls.from_expr(expr=select_expr, uid_gen=uid_gen) @classmethod def from_union( @@ -227,21 +292,8 @@ def from_union( assert ( len(list(selects)) >= 2 ), f"At least two select expressions must be provided, but got {selects}." - - existing_ctes: list[sge.CTE] = [] - union_selects: list[sge.Select] = [] - for select in selects: - assert isinstance( - select, sge.Select - ), f"All provided expressions must be of type sge.Select, but got {type(select)}" - - select_expr = select.copy() - select_expr, select_ctes = _pop_query_ctes(select_expr) - existing_ctes = _merge_ctes(existing_ctes, select_ctes) - union_selects.append(select_expr) - - union_expr: sge.Query = union_selects[0].subquery() - for select in union_selects[1:]: + union_expr: sge.Query = selects[0].subquery() + for select in selects[1:]: union_expr = sge.Union( this=union_expr, expression=select.subquery(), @@ -251,16 +303,15 @@ def from_union( selections = [ sge.Alias( - this=sge.to_identifier(old_name, quoted=cls.quoted), - alias=sge.to_identifier(new_name, quoted=cls.quoted), + this=sql.identifier(old_name), + alias=sql.identifier(new_name), ) for old_name, new_name in output_aliases ] final_select_expr = ( sge.Select().select(*selections).from_(union_expr.subquery()) ) - final_select_expr = _set_query_ctes(final_select_expr, existing_ctes) - return cls(expr=final_select_expr, uid_gen=uid_gen) + return cls.from_expr(expr=final_select_expr, uid_gen=uid_gen) def join( self, @@ -271,12 +322,8 @@ def join( joins_nulls: bool = True, ) -> SQLGlotIR: """Joins the current query with another SQLGlotIR instance.""" - left_select, left_cte_name = self._select_to_cte() - right_select, right_cte_name = right._select_to_cte() - - left_select, left_ctes = _pop_query_ctes(left_select) - right_select, right_ctes = _pop_query_ctes(right_select) - merged_ctes = _merge_ctes(left_ctes, right_ctes) + left_from = self.expr.as_from_item() + right_from = right.expr.as_from_item() join_on = _and( tuple( @@ -285,15 +332,12 @@ def join( ) join_type_str = join_type if join_type != "outer" else "full outer" - new_expr = ( - sge.Select() - .select(sge.Star()) - .from_(sge.Table(this=left_cte_name)) - .join(sge.Table(this=right_cte_name), on=join_on, join_type=join_type_str) + return SQLGlotIR.from_func( + lambda select: select.from_(left_from).join( + right_from, on=join_on, join_type=join_type_str + ), + uid_gen=self.uid_gen, ) - new_expr = _set_query_ctes(new_expr, merged_ctes) - - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) def isin_join( self, @@ -303,55 +347,58 @@ def isin_join( joins_nulls: bool = True, ) -> SQLGlotIR: """Joins the current query with another SQLGlotIR instance.""" - left_select, left_cte_name = self._select_to_cte() - # Prefer subquery over CTE for the IN clause's right side to improve SQL readability. - right_select = right._as_select() - - left_select, left_ctes = _pop_query_ctes(left_select) - right_select, right_ctes = _pop_query_ctes(right_select) - merged_ctes = _merge_ctes(left_ctes, right_ctes) - - left_condition = typed_expr.TypedExpr( - sge.Column(this=conditions[0].expr, table=left_cte_name), - conditions[0].dtype, - ) + left_from = self.expr.as_from_item() new_column: sge.Expression if joins_nulls: - right_table_name = sge.to_identifier( - next(self.uid_gen.get_uid_stream("bft_")), quoted=self.quoted + force_float_domain = False + if ( + conditions[0].dtype == dtypes.FLOAT_DTYPE + or conditions[1].dtype == dtypes.FLOAT_DTYPE + ): + force_float_domain = True + part1_id = sql.identifier("bfpart1") + part2_id = sql.identifier("bfpart2") + left_expr1, left_expr2 = _value_to_non_null_identity( + conditions[0], force_float_domain ) - right_condition = typed_expr.TypedExpr( - sge.Column(this=conditions[1].expr, table=right_table_name), - conditions[1].dtype, + left_as_struct = sge.Struct( + expressions=[ + sge.PropertyEQ(this=part1_id, expression=left_expr1), + sge.PropertyEQ(this=part2_id, expression=left_expr2), + ] ) - new_column = sge.Exists( - this=sge.Select() - .select(sge.convert(1)) - .from_(sge.Alias(this=right_select.subquery(), alias=right_table_name)) - .where( - _join_condition(left_condition, right_condition, joins_nulls=True) - ) + right_expr1, right_expr2 = _value_to_non_null_identity( + conditions[1], force_float_domain ) - else: + right_select = right.expr.select( + *[ + sge.Struct( + expressions=[ + sge.PropertyEQ(this=part1_id, expression=right_expr1), + sge.PropertyEQ(this=part2_id, expression=right_expr2), + ] + ) + ], + ) + new_column = sge.In( - this=left_condition.expr, + this=left_as_struct, expressions=[right_select.subquery()], ) + else: + new_column = sge.In( + this=conditions[0].expr, + expressions=[right._as_subquery()], + ) new_column = sge.Alias( this=new_column, - alias=sge.to_identifier(indicator_col, quoted=self.quoted), - ) - - new_expr = ( - sge.Select() - .select(sge.Column(this=sge.Star(), table=left_cte_name), new_column) - .from_(sge.Table(this=left_cte_name)) + alias=sql.identifier(indicator_col), ) - new_expr = _set_query_ctes(new_expr, merged_ctes) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) + new_expr = sge.Select().select(sge.Star(), new_column).from_(left_from) + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) def explode( self, @@ -370,11 +417,11 @@ def sample(self, fraction: float) -> SQLGlotIR: """Uniform samples a fraction of the rows.""" condition = sge.LT( this=sge.func("RAND"), - expression=_literal(fraction, dtypes.FLOAT_DTYPE), + expression=sql.literal(fraction, dtypes.FLOAT_DTYPE), ) - new_expr = self._select_to_cte()[0].where(condition, append=False) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) + new_expr = self.expr.as_select_all().where(condition, append=False) + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) def aggregate( self, @@ -392,15 +439,12 @@ def aggregate( aggregations_expr = [ sge.Alias( this=expr, - alias=sge.to_identifier(id, quoted=self.quoted), + alias=sql.identifier(id), ) for id, expr in aggregations ] - new_expr, _ = self._select_to_cte() - new_expr = new_expr.group_by(*by_cols).select( - *[*by_cols, *aggregations_expr], append=False - ) + new_expr = self.expr.select(*[*by_cols, *aggregations_expr]).group_by(*by_cols) condition = _and( tuple( @@ -410,7 +454,21 @@ def aggregate( ) if condition is not None: new_expr = new_expr.where(condition, append=False) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) + + def with_ctes( + self, + ctes: tuple[tuple[str, SQLGlotIR], ...], + ) -> SQLGlotIR: + sge_ctes = [ + sge.CTE( + this=cte.expr.as_select_all(), + alias=sql.identifier(cte_name), + ) + for cte_name, cte in ctes + ] + select_expr = _set_query_ctes(self.expr.as_select_all(), sge_ctes) + return SQLGlotIR.from_expr(expr=select_expr, uid_gen=self.uid_gen) def resample( self, @@ -420,83 +478,42 @@ def resample( stop_expr: sge.Expression, step_expr: sge.Expression, ) -> SQLGlotIR: - # Get identifier for left and right by pushing them to CTEs - left_select, left_id = self._select_to_cte() - right_select, right_id = right._select_to_cte() - - # Extract all CTEs from the returned select expressions - _, left_ctes = _pop_query_ctes(left_select) - _, right_ctes = _pop_query_ctes(right_select) - merged_ctes = _merge_ctes(left_ctes, right_ctes) - - generate_array = sge.func("GENERATE_ARRAY", start_expr, stop_expr, step_expr) + generate_array = sge.func( + "GENERATE_ARRAY", + start_expr, + stop_expr, + step_expr, + ) - unnested_column_alias = sge.to_identifier( - next(self.uid_gen.get_uid_stream("bfcol_")), quoted=self.quoted + unnested_column_alias = sql.identifier( + next(self.uid_gen.get_uid_stream("bfcol_")) ) unnest_expr = sge.Unnest( expressions=[generate_array], alias=sge.TableAlias(columns=[unnested_column_alias]), ) - final_col_id = sge.to_identifier(array_col_name, quoted=self.quoted) + final_col_id = sql.identifier(array_col_name) # Build final expression by joining everything directly in a single SELECT new_expr = ( sge.Select() .select(unnested_column_alias.as_(final_col_id)) - .from_(sge.Table(this=left_id)) - .join(sge.Table(this=right_id), join_type="cross") + .from_(self.expr.as_from_item()) + .join(right.expr.as_from_item(), join_type="cross") .join(unnest_expr, join_type="cross") ) - new_expr = _set_query_ctes(new_expr, merged_ctes) - - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) - - def insert( - self, - destination: bigquery.TableReference, - ) -> str: - """Generates an INSERT INTO SQL statement from the current SELECT clause.""" - return sge.insert(self._as_from_item(), _table(destination)).sql( - dialect=self.dialect, pretty=self.pretty - ) - def replace( - self, - destination: bigquery.TableReference, - ) -> str: - """Generates a MERGE statement to replace the destination table's contents. - by the current SELECT clause. - """ - # Workaround for SQLGlot breaking change: - # https://github.com/tobymao/sqlglot/pull/4495 - whens_expr = [ - sge.When(matched=False, source=True, then=sge.Delete()), - sge.When(matched=False, then=sge.Insert(this=sge.Var(this="ROW"))), - ] - whens_str = "\n".join( - when_expr.sql(dialect=self.dialect, pretty=self.pretty) - for when_expr in whens_expr - ) - - merge_str = sge.Merge( - this=_table(destination), - using=self._as_from_item(), - on=_literal(False, dtypes.BOOL_DTYPE), - ).sql(dialect=self.dialect, pretty=self.pretty) - return f"{merge_str}\n{whens_str}" + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) def _explode_single_column( self, column_name: str, offsets_col: typing.Optional[str] ) -> SQLGlotIR: """Helper method to handle the case of exploding a single column.""" - offset = ( - sge.to_identifier(offsets_col, quoted=self.quoted) if offsets_col else None - ) - column = sge.to_identifier(column_name, quoted=self.quoted) - unnested_column_alias = sge.to_identifier( - next(self.uid_gen.get_uid_stream("bfcol_")), quoted=self.quoted + offset = sql.identifier(offsets_col) if offsets_col else None + column = sql.identifier(column_name) + unnested_column_alias = sql.identifier( + next(self.uid_gen.get_uid_stream("bfcol_")) ) unnest_expr = sge.Unnest( expressions=[column], @@ -505,12 +522,9 @@ def _explode_single_column( ) selection = sge.Star(replace=[unnested_column_alias.as_(column)]) - new_expr, _ = self._select_to_cte() # Use LEFT JOIN to preserve rows when unnesting empty arrays. - new_expr = new_expr.select(selection, append=False).join( - unnest_expr, join_type="LEFT" - ) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) + new_expr = self.expr.select(selection).join(unnest_expr, join_type="LEFT") + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) def _explode_multiple_columns( self, @@ -518,27 +532,19 @@ def _explode_multiple_columns( offsets_col: typing.Optional[str], ) -> SQLGlotIR: """Helper method to handle the case of exploding multiple columns.""" - offset = ( - sge.to_identifier(offsets_col, quoted=self.quoted) if offsets_col else None - ) - columns = [ - sge.to_identifier(column_name, quoted=self.quoted) - for column_name in column_names - ] + offset = sql.identifier(offsets_col) if offsets_col else None + columns = [sql.identifier(column_name) for column_name in column_names] # If there are multiple columns, we need to unnest by zipping the arrays: # https://cloud.google.com/bigquery/docs/arrays#zipping_arrays - column_lengths = [ - sge.func("ARRAY_LENGTH", sge.to_identifier(column, quoted=self.quoted)) - 1 - for column in columns - ] + column_lengths = [sge.func("ARRAY_LENGTH", column) - 1 for column in columns] generate_array = sge.func( "GENERATE_ARRAY", sge.convert(0), sge.func("LEAST", *column_lengths), ) - unnested_offset_alias = sge.to_identifier( - next(self.uid_gen.get_uid_stream("bfcol_")), quoted=self.quoted + unnested_offset_alias = sql.identifier( + next(self.uid_gen.get_uid_stream("bfcol_")) ) unnest_expr = sge.Unnest( expressions=[generate_array], @@ -556,147 +562,13 @@ def _explode_multiple_columns( for column in columns ] ) - new_expr, _ = self._select_to_cte() # Use LEFT JOIN to preserve rows when unnesting empty arrays. - new_expr = new_expr.select(selection, append=False).join( - unnest_expr, join_type="LEFT" - ) - return SQLGlotIR(expr=new_expr, uid_gen=self.uid_gen) - - def _as_from_item(self) -> typing.Union[sge.Table, sge.Subquery]: - if isinstance(self.expr, sge.Select): - return self.expr.subquery() - else: # table - return self.expr - - def _as_select(self) -> sge.Select: - if isinstance(self.expr, sge.Select): - return self.expr - else: # table - return sge.Select().from_(self.expr) + new_expr = self.expr.select(selection).join(unnest_expr, join_type="LEFT") + return SQLGlotIR.from_expr(expr=new_expr, uid_gen=self.uid_gen) def _as_subquery(self) -> sge.Subquery: - return self._as_select().subquery() - - def _select_to_cte(self) -> tuple[sge.Select, sge.Identifier]: - """Transforms a given sge.Select query by pushing its main SELECT statement - into a new CTE and then generates a 'SELECT * FROM new_cte_name' - for the new query.""" - cte_name = sge.to_identifier( - next(self.uid_gen.get_uid_stream("bfcte_")), quoted=self.quoted - ) - select_expr = self._as_select().copy() - select_expr, existing_ctes = _pop_query_ctes(select_expr) - new_cte = sge.CTE( - this=select_expr, - alias=cte_name, - ) - new_select_expr = ( - sge.Select().select(sge.Star()).from_(sge.Table(this=cte_name)) - ) - new_select_expr = _set_query_ctes(new_select_expr, [*existing_ctes, new_cte]) - return new_select_expr, cte_name - - -def identifier(id: str) -> str: - """Return a string representing column reference in a SQL.""" - return sge.to_identifier(id, quoted=SQLGlotIR.quoted).sql(dialect=SQLGlotIR.dialect) - - -def _escape_chars(value: str): - """Escapes all special characters""" - # TODO: Reuse _literal's escaping logic instead of re-implementing it here. - # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals - trans_table = str.maketrans( - { - "\a": r"\a", - "\b": r"\b", - "\f": r"\f", - "\n": r"\n", - "\r": r"\r", - "\t": r"\t", - "\v": r"\v", - "\\": r"\\", - "?": r"\?", - '"': r"\"", - "'": r"\'", - "`": r"\`", - } - ) - return value.translate(trans_table) - - -def _is_null_literal(expr: sge.Expression) -> bool: - """Checks if the given expression is a NULL literal.""" - if isinstance(expr, sge.Null): - return True - if isinstance(expr, sge.Cast) and isinstance(expr.this, sge.Null): - return True - return False - - -def _literal(value: typing.Any, dtype: dtypes.Dtype) -> sge.Expression: - sqlglot_type = sgt.from_bigframes_dtype(dtype) if dtype else None - if sqlglot_type is None: - if not pd.isna(value): - raise ValueError(f"Cannot infer SQLGlot type from None dtype: {value}") - return sge.Null() - - if value is None: - return _cast(sge.Null(), sqlglot_type) - if 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) - elif pd.isna(value) or (isinstance(value, pa.Scalar) and not value.is_valid): - return _cast(sge.Null(), sqlglot_type) - elif dtype == dtypes.JSON_DTYPE: - return sge.ParseJSON(this=sge.convert(str(value))) - elif dtype == dtypes.BYTES_DTYPE: - return _cast(str(value), sqlglot_type) - elif dtypes.is_time_like(dtype): - if isinstance(value, str): - return _cast(sge.convert(value), sqlglot_type) - if isinstance(value, np.generic): - value = value.item() - return _cast(sge.convert(value.isoformat()), sqlglot_type) - elif dtype in (dtypes.NUMERIC_DTYPE, dtypes.BIGNUMERIC_DTYPE): - return _cast(sge.convert(value), 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.TIMEDELTA_DTYPE: - return sge.convert(utils.timedelta_to_micros(value)) - elif dtype == dtypes.FLOAT_DTYPE: - if np.isinf(value): - return constants._INF if value > 0 else constants._NEG_INF - return sge.convert(value) - else: - if isinstance(value, np.generic): - value = value.item() - return sge.convert(value) - - -def _cast(arg: typing.Any, to: str) -> sge.Cast: - return sge.Cast(this=arg, to=to) - - -def _table(table: bigquery.TableReference) -> sge.Table: - return sge.Table( - this=sg.to_identifier(table.table_id, quoted=True), - db=sg.to_identifier(table.dataset_id, quoted=True), - catalog=sg.to_identifier(table.project, quoted=True), - ) + # Sometimes explicitly need a subquery, e.g. for IN expressions. + return self.expr.as_select_all().subquery() def _and(conditions: tuple[sge.Expression, ...]) -> typing.Optional[sge.Expression]: @@ -729,77 +601,57 @@ def _join_condition( joins_nulls: If True, generates complex logic to handle nulls/NaNs. Otherwise, uses a simple equality check where appropriate. """ - is_floating_types = ( - left.dtype == dtypes.FLOAT_DTYPE and right.dtype == dtypes.FLOAT_DTYPE - ) - if not is_floating_types and not joins_nulls: + if not joins_nulls: return sge.EQ(this=left.expr, expression=right.expr) - is_numeric_types = dtypes.is_numeric( - left.dtype, include_bool=False - ) and dtypes.is_numeric(right.dtype, include_bool=False) - if is_numeric_types: - return _join_condition_for_numeric(left, right) - else: - return _join_condition_for_others(left, right) - - -def _join_condition_for_others( - left: typed_expr.TypedExpr, - right: typed_expr.TypedExpr, -) -> sge.And: - """Generates a join condition for non-numeric types to match pandas's - null-handling logic. - """ - left_str = _cast(left.expr, "STRING") - right_str = _cast(right.expr, "STRING") - left_0 = sge.func("COALESCE", left_str, _literal("0", dtypes.STRING_DTYPE)) - left_1 = sge.func("COALESCE", left_str, _literal("1", dtypes.STRING_DTYPE)) - right_0 = sge.func("COALESCE", right_str, _literal("0", dtypes.STRING_DTYPE)) - right_1 = sge.func("COALESCE", right_str, _literal("1", dtypes.STRING_DTYPE)) + force_float_domain = False + if left.dtype == dtypes.FLOAT_DTYPE or right.dtype == dtypes.FLOAT_DTYPE: + force_float_domain = True + left_expr1, left_expr2 = _value_to_non_null_identity(left, force_float_domain) + right_expr1, right_expr2 = _value_to_non_null_identity(right, force_float_domain) return sge.And( - this=sge.EQ(this=left_0, expression=right_0), - expression=sge.EQ(this=left_1, expression=right_1), + this=sge.EQ(this=left_expr1, expression=right_expr1), + expression=sge.EQ(this=left_expr2, expression=right_expr2), ) -def _join_condition_for_numeric( - left: typed_expr.TypedExpr, - right: typed_expr.TypedExpr, -) -> sge.And: - """Generates a join condition for non-numeric types to match pandas's - null-handling logic. Specifically for FLOAT types, Pandas treats NaN aren't - equal so need to coalesce as well with different constants. - """ - is_floating_types = ( - left.dtype == dtypes.FLOAT_DTYPE and right.dtype == dtypes.FLOAT_DTYPE - ) - left_0 = sge.func("COALESCE", left.expr, _literal(0, left.dtype)) - left_1 = sge.func("COALESCE", left.expr, _literal(1, left.dtype)) - right_0 = sge.func("COALESCE", right.expr, _literal(0, right.dtype)) - right_1 = sge.func("COALESCE", right.expr, _literal(1, right.dtype)) - if not is_floating_types: - return sge.And( - this=sge.EQ(this=left_0, expression=right_0), - expression=sge.EQ(this=left_1, expression=right_1), +def _value_to_non_null_identity( + value: typed_expr.TypedExpr, force_float_domain: bool = False +) -> tuple[sge.Expression, sge.Expression]: + # normal_value -> (normal_value, normal_value) + # null_value -> (0, 1) + # nan_value -> (2, 3) + if dtypes.is_numeric(value.dtype, include_bool=False): + dtype = dtypes.FLOAT_DTYPE if force_float_domain else value.dtype + expr1 = sge.func( + "COALESCE", value.expr, sql.literal(0.0 if force_float_domain else 0, dtype) + ) + expr2 = sge.func( + "COALESCE", value.expr, sql.literal(1.0 if force_float_domain else 1, dtype) + ) + if value.dtype == dtypes.FLOAT_DTYPE: + expr1 = sge.If( + this=sge.IsNan(this=value.expr), + true=sql.literal(2.0, value.dtype), + false=expr1, + ) + expr2 = sge.If( + this=sge.IsNan(this=value.expr), + true=sql.literal(3, value.dtype), + false=expr2, + ) + else: # general case, convert to string and coalesce + expr1 = sge.func( + "COALESCE", + sql.cast(value.expr, "STRING"), + sql.literal("0", dtypes.STRING_DTYPE), ) - - left_2 = sge.If( - this=sge.IsNan(this=left.expr), true=_literal(2, left.dtype), false=left_0 - ) - left_3 = sge.If( - this=sge.IsNan(this=left.expr), true=_literal(3, left.dtype), false=left_1 - ) - right_2 = sge.If( - this=sge.IsNan(this=right.expr), true=_literal(2, right.dtype), false=right_0 - ) - right_3 = sge.If( - this=sge.IsNan(this=right.expr), true=_literal(3, right.dtype), false=right_1 - ) - return sge.And( - this=sge.EQ(this=left_2, expression=right_2), - expression=sge.EQ(this=left_3, expression=right_3), - ) + expr2 = sge.func( + "COALESCE", + sql.cast(value.expr, "STRING"), + sql.literal("1", dtypes.STRING_DTYPE), + ) + return expr1, expr2 def _set_query_ctes( @@ -817,26 +669,3 @@ def _set_query_ctes( else: raise ValueError("The expression does not support CTEs.") return new_expr - - -def _merge_ctes(ctes1: list[sge.CTE], ctes2: list[sge.CTE]) -> list[sge.CTE]: - """Merges two lists of CTEs, de-duplicating by alias name.""" - seen = {cte.alias: cte for cte in ctes1} - for cte in ctes2: - if cte.alias not in seen: - seen[cte.alias] = cte - return list(seen.values()) - - -def _pop_query_ctes( - expr: sge.Select, -) -> tuple[sge.Select, list[sge.CTE]]: - """Pops the CTEs of a given sge.Select expression.""" - if "with" in expr.arg_types.keys(): - expr_ctes = expr.args.pop("with", []) - return expr, expr_ctes - elif "with_" in expr.arg_types.keys(): - expr_ctes = expr.args.pop("with_", []) - return expr, expr_ctes - else: - raise ValueError("The expression does not support CTEs.") diff --git a/bigframes/core/nodes.py b/bigframes/core/nodes.py index 4b1efcb285c..6071eaeaea2 100644 --- a/bigframes/core/nodes.py +++ b/bigframes/core/nodes.py @@ -1713,6 +1713,39 @@ def _node_expressions(self): return tuple(ref for ref, _ in self.output_cols) +@dataclasses.dataclass(frozen=True, eq=False) +class CteNode(UnaryNode): + """ + Semantically a no-op, used to indicate shared subtrees and act as optimization boundary. + """ + + @property + def fields(self) -> Sequence[Field]: + return self.child.fields + + @property + def variables_introduced(self) -> int: + return 0 + + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + + @property + def node_defined_ids(self) -> Tuple[identifiers.ColumnId, ...]: + return () + + def remap_vars( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> CteNode: + return self + + def remap_refs( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> CteNode: + return self + + # Tree operators def top_down( root: BigFrameNode, diff --git a/bigframes/core/pyformat.py b/bigframes/core/pyformat.py index 8f49556ff4c..1dbb74fbb72 100644 --- a/bigframes/core/pyformat.py +++ b/bigframes/core/pyformat.py @@ -89,7 +89,7 @@ def _field_to_template_value( dry_run: bool = False, ) -> str: """Convert value to something embeddable in a SQL string.""" - import bigframes.core.sql # Avoid circular imports + import bigframes.core.compile.sqlglot.sql as sql # Avoid circular imports import bigframes.dataframe # Avoid circular imports _validate_type(name, value) @@ -107,20 +107,20 @@ def _field_to_template_value( if isinstance(value, str): return value - return bigframes.core.sql.simple_literal(value) + return sql.to_sql(sql.literal(value)) def _validate_type(name: str, value: Any): """Raises TypeError if value is unsupported.""" - import bigframes.core.sql # Avoid circular imports import bigframes.dataframe # Avoid circular imports + import bigframes.dtypes # Avoid circular imports if value is None: return # None can't be used in isinstance, but is a valid literal. supported_types = ( typing.get_args(_BQ_TABLE_TYPES) - + typing.get_args(bigframes.core.sql.SIMPLE_LITERAL_TYPES) + + bigframes.dtypes.SUPPORTED_LITERAL_TYPES + (bigframes.dataframe.DataFrame,) + (pandas.DataFrame,) ) diff --git a/bigframes/core/rewrite/__init__.py b/bigframes/core/rewrite/__init__.py index a120612aae5..5279418f5fb 100644 --- a/bigframes/core/rewrite/__init__.py +++ b/bigframes/core/rewrite/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from bigframes.core.rewrite.as_sql import as_sql_nodes +from bigframes.core.rewrite.ctes import extract_ctes 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 @@ -34,6 +35,7 @@ __all__ = [ "as_sql_nodes", + "extract_ctes", "legacy_join_as_projection", "try_row_join", "rewrite_slice", diff --git a/bigframes/core/rewrite/as_sql.py b/bigframes/core/rewrite/as_sql.py index 32d677f75d7..2082eaa0687 100644 --- a/bigframes/core/rewrite/as_sql.py +++ b/bigframes/core/rewrite/as_sql.py @@ -14,11 +14,13 @@ from __future__ import annotations import dataclasses +import itertools from typing import Optional, Sequence, Union from bigframes.core import ( agg_expressions, expression, + guid, identifiers, nodes, ordering, @@ -222,6 +224,83 @@ def _as_sql_node(node: nodes.BigFrameNode) -> nodes.BigFrameNode: return node -def as_sql_nodes(root: nodes.BigFrameNode) -> nodes.BigFrameNode: - # TODO: Aggregations, Unions, Joins, raw data sources - return nodes.bottom_up(root, _as_sql_node) +# In the future, we will have sql nodes for each of these node types. +_LOGICAL_NODE_TYPES_TO_WRAP = ( + nodes.ReadLocalNode, + nodes.ExplodeNode, + nodes.InNode, + nodes.AggregateNode, + nodes.FromRangeNode, + nodes.ConcatNode, + sql_nodes.SqlSelectNode, +) + + +def _insert_cte_markers(root: nodes.BigFrameNode) -> nodes.BigFrameNode: + # important not to wrap nodes that are already wrapped + wrapped_nodes = set( + node.child for node in root.unique_nodes() if isinstance(node, nodes.CteNode) + ) + # don't wrap child nodes of ConcatNode + union_child_nodes = set( + itertools.chain.from_iterable( + node.child_nodes + for node in root.unique_nodes() + if isinstance(node, nodes.ConcatNode) + ) + ) + + def maybe_insert_cte_marker(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + if node == root: + return node + if ( + isinstance(node, _LOGICAL_NODE_TYPES_TO_WRAP) + and node not in wrapped_nodes + and node not in union_child_nodes + ): + wrapped_nodes.add(node) + return nodes.CteNode(node) + return node + + return root.top_down(maybe_insert_cte_marker) + + +def _extract_ctes_to_with_expr( + root: nodes.BigFrameNode, uid_gen: guid.SequentialUIDGenerator +) -> nodes.BigFrameNode: + topological_ctes = list( + filter( + lambda n: isinstance(n, nodes.CteNode), + root.iter_nodes_topo(), + ) + ) + cte_names = tuple( + next(uid_gen.get_uid_stream("bfcte_")) for _ in range(len(topological_ctes)) + ) + + if len(topological_ctes) == 0: + return root + + mapping = { + cte_node: sql_nodes.SqlCteRefNode(cte_name, tuple(cte_node.fields)) + for cte_node, cte_name in zip(topological_ctes, cte_names) + } + + # Replace all CTEs with CTE references and wrap the new root in a WITH clause + return sql_nodes.SqlWithCtesNode( + root.top_down(lambda x: mapping.get(x, x)), + cte_names, + tuple( + cte_node.child.top_down(lambda x: mapping.get(x, x)) for cte_node in topological_ctes # type: ignore + ), + ) + + +def as_sql_nodes( + root: nodes.BigFrameNode, uid_gen: guid.SequentialUIDGenerator +) -> nodes.BigFrameNode: + root = nodes.bottom_up(root, _as_sql_node) + # Insert CTE markers to indicate where we want to split the query. + root = _insert_cte_markers(root) + root = _extract_ctes_to_with_expr(root, uid_gen) + return root diff --git a/bigframes/core/rewrite/ctes.py b/bigframes/core/rewrite/ctes.py new file mode 100644 index 00000000000..a5afd19bb35 --- /dev/null +++ b/bigframes/core/rewrite/ctes.py @@ -0,0 +1,41 @@ +# 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 collections import defaultdict + +from bigframes.core import nodes + + +def extract_ctes(root: nodes.BigFrameNode) -> nodes.BigFrameNode: + # identify candidates + node_parents: dict[nodes.BigFrameNode, int] = defaultdict(int) + for parent in root.unique_nodes(): + for child in parent.child_nodes: + node_parents[child] += 1 + + # everywhere a multi-parent node is referenced, wrap it in a CTE node + def insert_cte_markers(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + def _add_cte_if_needed(child: nodes.BigFrameNode) -> nodes.BigFrameNode: + if node_parents[child] > 1: + return nodes.CteNode(child) + return child + + if isinstance(node, nodes.CteNode): + # don't re-wrap CTE nodes + return node + + return node.transform_children(_add_cte_if_needed) + + return root.top_down(insert_cte_markers) diff --git a/bigframes/core/rewrite/identifiers.py b/bigframes/core/rewrite/identifiers.py index 8efcbb4a0b9..7b1d1d9a512 100644 --- a/bigframes/core/rewrite/identifiers.py +++ b/bigframes/core/rewrite/identifiers.py @@ -13,13 +13,42 @@ # limitations under the License. from __future__ import annotations -import dataclasses import typing from bigframes.core import identifiers, nodes -# TODO: May as well just outright remove selection nodes in this process. +def _create_mapping_operator( + id_def_remapping_by_node: dict[ + nodes.BigFrameNode, dict[identifiers.ColumnId, identifiers.ColumnId] + ], + id_ref_remapping_by_node: dict[ + nodes.BigFrameNode, dict[identifiers.ColumnId, identifiers.ColumnId] + ], +): + """ + Builds a remapping operator that uses predefined local remappings for ids. + + Args: + id_remapping_by_node: A mapping from nodes to their local remappings. + + Returns: + A remapping operator. + """ + + def _mapping_operator(node: nodes.BigFrameNode) -> nodes.BigFrameNode: + # Step 1: Get the local remapping for the current node. + local_def_remaps = id_def_remapping_by_node[node] + local_ref_remaps = id_ref_remapping_by_node[node] + + result = node.remap_vars(local_def_remaps) + result = result.remap_refs(local_ref_remaps) + + return result + + return _mapping_operator + + def remap_variables( root: nodes.BigFrameNode, id_generator: typing.Iterator[identifiers.ColumnId], @@ -42,46 +71,47 @@ def remap_variables( A tuple of the new root node and a mapping from old to new column IDs visible to the parent node. """ - # Step 1: Recursively remap children to get their new nodes and ID mappings. - new_child_nodes: list[nodes.BigFrameNode] = [] - new_child_mappings: list[dict[identifiers.ColumnId, identifiers.ColumnId]] = [] - for child in root.child_nodes: - new_child, child_mappings = remap_variables(child, id_generator=id_generator) - new_child_nodes.append(new_child) - new_child_mappings.append(child_mappings) - - # Step 2: Transform children to use their new nodes. - remapped_children: dict[nodes.BigFrameNode, nodes.BigFrameNode] = { - child: new_child for child, new_child in zip(root.child_nodes, new_child_nodes) - } - new_root = root.transform_children(lambda node: remapped_children[node]) - - # Step 3: Transform the current node using the mappings from its children. - if isinstance(new_root, nodes.InNode): - new_root = typing.cast(nodes.InNode, new_root) - new_root = dataclasses.replace( - new_root, - left_col=new_root.left_col.remap_column_refs( - new_child_mappings[0], allow_partial_bindings=True - ), - ) - else: - downstream_mappings: dict[identifiers.ColumnId, identifiers.ColumnId] = { - k: v for mapping in new_child_mappings for k, v in mapping.items() - } - new_root = new_root.remap_refs(downstream_mappings) + # step 1: defined remappings for each individual unique node + # step 2: top down traversal to apply remappings (mappings are value-based, so bottom-up doesn't work) - # Step 4: Create new IDs for columns defined by the current node. - node_defined_mappings = { - old_id: next(id_generator) for old_id in root.node_defined_ids - } - new_root = new_root.remap_vars(node_defined_mappings) + id_def_remaps: dict[ + nodes.BigFrameNode, dict[identifiers.ColumnId, identifiers.ColumnId] + ] = {} + id_ref_remaps: dict[ + nodes.BigFrameNode, dict[identifiers.ColumnId, identifiers.ColumnId] + ] = {} + for node in root.iter_nodes_topo(): # bottom up + local_def_remaps = { + col_id: next(id_generator) for col_id in node.node_defined_ids + } + id_def_remaps[node] = local_def_remaps - new_root._validate() + local_ref_remaps = {} - # Step 5: Determine which mappings to propagate up to the parent. - propagated_mappings = { - old_id: new_id for old_id, new_id in zip(root.ids, new_root.ids) - } + # InNode is special case as ID scope inherited purely from left side + inheriting_nodes = ( + [node.child_nodes[0]] + if isinstance(node, nodes.InNode) + else node.child_nodes + ) + for child in inheriting_nodes: # inherit ref and def mappings from children + if not child.defines_namespace: # these nodes represent new id spaces + local_ref_remaps.update( + { + old_id: new_id + for old_id, new_id in id_ref_remaps[child].items() + if old_id in child.ids + } + ) + local_ref_remaps.update(id_def_remaps[child]) + id_ref_remaps[node] = local_ref_remaps - return new_root, propagated_mappings + # have to do top down to preserve node identities + return ( + root.top_down(_create_mapping_operator(id_def_remaps, id_ref_remaps)), + # Only used by unit tests + { + old_id: (id_def_remaps[root] | id_ref_remaps[root])[old_id] + for old_id in root.ids + }, + ) diff --git a/bigframes/core/rewrite/pruning.py b/bigframes/core/rewrite/pruning.py index 7695ace3b33..60400821c63 100644 --- a/bigframes/core/rewrite/pruning.py +++ b/bigframes/core/rewrite/pruning.py @@ -67,7 +67,7 @@ def prune_selection_child( # Important to check this first if list(selection.ids) == list(child.ids): - if (ref.ref.id == ref.id for ref in selection.input_output_pairs): + if all(ref.ref.id == ref.id for ref in selection.input_output_pairs): # selection is no-op so just remove it entirely return child @@ -75,6 +75,7 @@ def prune_selection_child( return selection.remap_refs( {id: ref.id for ref, id in child.input_output_pairs} ).replace_child(child.child) + elif isinstance(child, nodes.AdditiveNode): if not set(field.id for field in child.added_fields) & selection.consumed_ids: return selection.replace_child(child.additive_base) diff --git a/bigframes/core/sql/__init__.py b/bigframes/core/sql/__init__.py index b025ca07c27..8c9a093802c 100644 --- a/bigframes/core/sql/__init__.py +++ b/bigframes/core/sql/__init__.py @@ -17,16 +17,21 @@ Utility functions for SQL construction. """ -import datetime -import decimal import json -import math -from typing import cast, Collection, Iterable, Mapping, Optional, TYPE_CHECKING, Union +from typing import ( + Any, + cast, + Collection, + Iterable, + Mapping, + Optional, + TYPE_CHECKING, + Union, +) import bigframes_vendored.sqlglot.expressions as sge -import shapely.geometry.base # type: ignore -from bigframes.core.compile.sqlglot import sqlglot_ir +from bigframes.core.compile.sqlglot import sql if TYPE_CHECKING: import google.cloud.bigquery as bigquery @@ -43,68 +48,8 @@ to_wkt = dumps -SIMPLE_LITERAL_TYPES = Union[ - bytes, - str, - int, - bool, - float, - datetime.datetime, - datetime.date, - datetime.time, - decimal.Decimal, - list, -] - - -### Writing SQL Values (literals, column references, table references, etc.) -def simple_literal(value: Union[SIMPLE_LITERAL_TYPES, None]) -> str: - """Return quoted input string.""" - - # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#literals - if value is None: - return "NULL" - elif isinstance(value, str): - # Single quoting seems to work nicer with ibis than double quoting - return f"'{sqlglot_ir._escape_chars(value)}'" - elif isinstance(value, bytes): - return repr(value) - elif isinstance(value, (bool, int)): - return str(value) - elif isinstance(value, float): - # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#floating_point_literals - if math.isnan(value): - return 'CAST("nan" as FLOAT)' - if value == math.inf: - return 'CAST("+inf" as FLOAT)' - if value == -math.inf: - return 'CAST("-inf" as FLOAT)' - return str(value) - # Check datetime first as it is a subclass of date - elif isinstance(value, datetime.datetime): - if value.tzinfo is None: - return f"DATETIME('{value.isoformat()}')" - else: - return f"TIMESTAMP('{value.isoformat()}')" - elif isinstance(value, datetime.date): - return f"DATE('{value.isoformat()}')" - elif isinstance(value, datetime.time): - return f"TIME(DATETIME('1970-01-01 {value.isoformat()}'))" - elif isinstance(value, shapely.geometry.base.BaseGeometry): - return f"ST_GEOGFROMTEXT({simple_literal(to_wkt(value))})" - 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}") - - -def multi_literal(*values: str): - literal_strings = [simple_literal(i) for i in values] +def multi_literal(*values: Any): + literal_strings = [sql.to_sql(sql.literal(i)) for i in values] return "(" + ", ".join(literal_strings) + ")" @@ -119,7 +64,7 @@ def cast_as_string(column_name: str) -> str: def to_json_string(column_name: str) -> str: """Return a string representing JSON version of a column.""" - return f"TO_JSON_STRING({sqlglot_ir.identifier(column_name)})" + return f"TO_JSON_STRING({sql.to_sql(sql.identifier(column_name))})" def csv(values: Iterable[str]) -> str: @@ -202,7 +147,7 @@ def create_vector_index_ddl( if len(stored_column_names) > 0: escaped_stored = [ - f"{sqlglot_ir.identifier(name)}" for name in stored_column_names + f"{sql.to_sql(sql.identifier(name))}" for name in stored_column_names ] storing = f"STORING({', '.join(escaped_stored)}) " else: @@ -210,14 +155,14 @@ def create_vector_index_ddl( rendered_options = ", ".join( [ - f"{option_name} = {simple_literal(option_value)}" + f"{option_name} = {sql.to_sql(sql.literal(option_value))}" for option_name, option_value in options.items() ] ) return f""" - {create} {sqlglot_ir.identifier(index_name)} - ON {sqlglot_ir.identifier(table_name)}({sqlglot_ir.identifier(column_name)}) + {create} {sql.to_sql(sql.identifier(index_name))} + ON {sql.to_sql(sql.identifier(table_name))}({sql.to_sql(sql.identifier(column_name))}) {storing} OPTIONS({rendered_options}); """ @@ -236,25 +181,27 @@ def create_vector_search_sql( """Encode the VECTOR SEARCH statement for BigQuery Vector Search.""" vector_search_args = [ - f"TABLE {sqlglot_ir.identifier(cast(str, base_table))}", - f"{simple_literal(column_to_search)}", + f"TABLE {sql.to_sql(sql.identifier(cast(str, base_table)))}", + f"{sql.to_sql(sql.literal(column_to_search))}", f"({sql_string})", ] if query_column_to_search is not None: vector_search_args.append( - f"query_column_to_search => {simple_literal(query_column_to_search)}" + f"query_column_to_search => {sql.to_sql(sql.literal(query_column_to_search))}" ) if top_k is not None: - vector_search_args.append(f"top_k=> {simple_literal(top_k)}") + vector_search_args.append(f"top_k=> {sql.to_sql(sql.literal(top_k))}") if distance_type is not None: - vector_search_args.append(f"distance_type => {simple_literal(distance_type)}") + vector_search_args.append( + f"distance_type => {sql.to_sql(sql.literal(distance_type))}" + ) if options is not None: vector_search_args.append( - f"options => {simple_literal(json.dumps(options, indent=None))}" + f"options => {sql.to_sql(sql.literal(json.dumps(options, indent=None)))}" ) args_str = ",\n".join(vector_search_args) diff --git a/bigframes/core/sql/io.py b/bigframes/core/sql/io.py deleted file mode 100644 index 9e1a549a64f..00000000000 --- a/bigframes/core/sql/io.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2026 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 Mapping, Optional, Union - - -def load_data_ddl( - table_name: str, - *, - write_disposition: str = "INTO", - columns: Optional[Mapping[str, str]] = None, - partition_by: Optional[list[str]] = None, - cluster_by: Optional[list[str]] = None, - table_options: Optional[Mapping[str, Union[str, int, float, bool, list]]] = None, - from_files_options: Mapping[str, Union[str, int, float, bool, list]], - with_partition_columns: Optional[Mapping[str, str]] = None, - connection_name: Optional[str] = None, -) -> str: - """Generates the LOAD DATA DDL statement.""" - statement = ["LOAD DATA"] - statement.append(write_disposition) - statement.append(table_name) - - if columns: - column_defs = ", ".join([f"{name} {typ}" for name, typ in columns.items()]) - statement.append(f"({column_defs})") - - if partition_by: - statement.append(f"PARTITION BY {', '.join(partition_by)}") - - if cluster_by: - statement.append(f"CLUSTER BY {', '.join(cluster_by)}") - - if table_options: - opts = [] - for key, value in table_options.items(): - if isinstance(value, str): - value_sql = repr(value) - opts.append(f"{key} = {value_sql}") - elif isinstance(value, bool): - opts.append(f"{key} = {str(value).upper()}") - elif isinstance(value, list): - list_str = ", ".join([repr(v) for v in value]) - opts.append(f"{key} = [{list_str}]") - else: - opts.append(f"{key} = {value}") - options_str = ", ".join(opts) - statement.append(f"OPTIONS ({options_str})") - - opts = [] - for key, value in from_files_options.items(): - if isinstance(value, str): - value_sql = repr(value) - opts.append(f"{key} = {value_sql}") - elif isinstance(value, bool): - opts.append(f"{key} = {str(value).upper()}") - elif isinstance(value, list): - list_str = ", ".join([repr(v) for v in value]) - opts.append(f"{key} = [{list_str}]") - else: - opts.append(f"{key} = {value}") - options_str = ", ".join(opts) - statement.append(f"FROM FILES ({options_str})") - - if with_partition_columns: - part_defs = ", ".join( - [f"{name} {typ}" for name, typ in with_partition_columns.items()] - ) - statement.append(f"WITH PARTITION COLUMNS ({part_defs})") - - if connection_name: - statement.append(f"WITH CONNECTION `{connection_name}`") - - return " ".join(statement) diff --git a/bigframes/core/sql/literals.py b/bigframes/core/sql/literals.py deleted file mode 100644 index 59c81977315..00000000000 --- a/bigframes/core/sql/literals.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2026 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 json -from typing import Any, List, Mapping, Union - -import bigframes.core.sql - -STRUCT_VALUES = Union[ - str, int, float, bool, Mapping[str, str], List[str], Mapping[str, Any] -] -STRUCT_TYPE = Mapping[str, STRUCT_VALUES] - - -def struct_literal(struct_options: STRUCT_TYPE) -> str: - rendered_options = [] - for option_name, option_value in struct_options.items(): - if option_name == "model_params": - json_str = json.dumps(option_value) - # Escape single quotes for SQL string literal - sql_json_str = json_str.replace("'", "''") - rendered_val = f"JSON'{sql_json_str}'" - elif isinstance(option_value, collections.abc.Mapping): - struct_body = ", ".join( - [ - f"{bigframes.core.sql.simple_literal(v)} AS {k}" - for k, v in option_value.items() - ] - ) - rendered_val = f"STRUCT({struct_body})" - elif isinstance(option_value, list): - rendered_val = ( - "[" - + ", ".join( - [bigframes.core.sql.simple_literal(v) for v in option_value] - ) - + "]" - ) - elif isinstance(option_value, bool): - rendered_val = str(option_value).lower() - else: - rendered_val = bigframes.core.sql.simple_literal(option_value) - rendered_options.append(f"{rendered_val} AS {option_name}") - return f"STRUCT({', '.join(rendered_options)})" diff --git a/bigframes/core/sql/ml.py b/bigframes/core/sql/ml.py index 38d66ab9a56..93ccca6aa15 100644 --- a/bigframes/core/sql/ml.py +++ b/bigframes/core/sql/ml.py @@ -16,9 +16,7 @@ from typing import Any, Dict, List, Mapping, Optional, Union -from bigframes.core.compile.sqlglot import sqlglot_ir -import bigframes.core.sql -import bigframes.core.sql.literals +from bigframes.core.compile.sqlglot import sql as sg_sql def create_model_ddl( @@ -46,7 +44,7 @@ def create_model_ddl( else: create = "CREATE MODEL " - ddl = f"{create}{sqlglot_ir.identifier(model_name)}\n" + ddl = f"{create}{sg_sql.to_sql(sg_sql.identifier(model_name))}\n" # [TRANSFORM (select_list)] if transform: @@ -66,7 +64,7 @@ def create_model_ddl( if connection_name.upper() == "DEFAULT": ddl += "REMOTE WITH CONNECTION DEFAULT\n" else: - ddl += f"REMOTE WITH CONNECTION {sqlglot_ir.identifier(connection_name)}\n" + ddl += f"REMOTE WITH CONNECTION {sg_sql.to_sql(sg_sql.identifier(connection_name))}\n" # [OPTIONS(model_option_list)] if options: @@ -76,9 +74,9 @@ def create_model_ddl( # Handle list options like model_registry="vertex_ai" # wait, usually options are key=value. # if value is list, it is [val1, val2] - rendered_val = bigframes.core.sql.simple_literal(list(option_value)) + rendered_val = sg_sql.to_sql(sg_sql.literal(list(option_value))) else: - rendered_val = bigframes.core.sql.simple_literal(option_value) + rendered_val = sg_sql.to_sql(sg_sql.literal(option_value)) rendered_options.append(f"{option_name} = {rendered_val}") @@ -108,7 +106,7 @@ def _build_struct_sql( ) -> str: if not struct_options: return "" - return f", {bigframes.core.sql.literals.struct_literal(struct_options)}" + return f", {sg_sql.to_sql(sg_sql.literal(struct_options))}" def evaluate( @@ -130,7 +128,7 @@ def evaluate( if confidence_level is not None: struct_options["confidence_level"] = confidence_level - sql = f"SELECT * FROM ML.EVALUATE(MODEL {sqlglot_ir.identifier(model_name)}" + sql = f"SELECT * FROM ML.EVALUATE(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}" if table: sql += f", ({table})" @@ -158,9 +156,7 @@ def predict( if trial_id is not None: struct_options["trial_id"] = trial_id - sql = ( - f"SELECT * FROM ML.PREDICT(MODEL {sqlglot_ir.identifier(model_name)}, ({table})" - ) + sql = f"SELECT * FROM ML.PREDICT(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}, ({table})" sql += _build_struct_sql(struct_options) sql += ")\n" return sql @@ -190,7 +186,7 @@ def explain_predict( if approx_feature_contrib is not None: struct_options["approx_feature_contrib"] = approx_feature_contrib - sql = f"SELECT * FROM ML.EXPLAIN_PREDICT(MODEL {sqlglot_ir.identifier(model_name)}, ({table})" + sql = f"SELECT * FROM ML.EXPLAIN_PREDICT(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}, ({table})" sql += _build_struct_sql(struct_options) sql += ")\n" return sql @@ -208,7 +204,7 @@ def global_explain( if class_level_explain is not None: struct_options["class_level_explain"] = class_level_explain - sql = f"SELECT * FROM ML.GLOBAL_EXPLAIN(MODEL {sqlglot_ir.identifier(model_name)}" + sql = f"SELECT * FROM ML.GLOBAL_EXPLAIN(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}" sql += _build_struct_sql(struct_options) sql += ")\n" return sql @@ -221,7 +217,7 @@ def transform( """Encode the ML.TRANSFORM statement. See https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-transform for reference. """ - sql = f"SELECT * FROM ML.TRANSFORM(MODEL {sqlglot_ir.identifier(model_name)}, ({table}))\n" + sql = f"SELECT * FROM ML.TRANSFORM(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}, ({table}))\n" return sql @@ -262,12 +258,22 @@ def generate_text( if request_type is not None: struct_options["request_type"] = request_type - sql = f"SELECT * FROM ML.GENERATE_TEXT(MODEL {sqlglot_ir.identifier(model_name)}, ({table})" + sql = f"SELECT * FROM ML.GENERATE_TEXT(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}, ({table})" sql += _build_struct_sql(struct_options) sql += ")\n" return sql +def get_insights( + model_name: str, +) -> str: + """Encode the ML.GET_INSIGHTS statement. + See https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-get-insights for reference. + """ + sql = f"SELECT * FROM ML.GET_INSIGHTS(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))})\n" + return sql + + def generate_embedding( model_name: str, table: str, @@ -290,7 +296,7 @@ def generate_embedding( if output_dimensionality is not None: struct_options["output_dimensionality"] = output_dimensionality - sql = f"SELECT * FROM ML.GENERATE_EMBEDDING(MODEL {sqlglot_ir.identifier(model_name)}, ({table})" + sql = f"SELECT * FROM ML.GENERATE_EMBEDDING(MODEL {sg_sql.to_sql(sg_sql.identifier(model_name))}, ({table})" sql += _build_struct_sql(struct_options) sql += ")\n" return sql diff --git a/bigframes/core/sql_nodes.py b/bigframes/core/sql_nodes.py index 5d921de7aeb..45048dc2b15 100644 --- a/bigframes/core/sql_nodes.py +++ b/bigframes/core/sql_nodes.py @@ -16,13 +16,18 @@ import dataclasses import functools -from typing import Mapping, Optional, Sequence, Tuple +from typing import Callable, Mapping, Optional, Sequence, Tuple from bigframes.core import bq_data, identifiers, nodes import bigframes.core.expression as ex from bigframes.core.ordering import OrderingExpression import bigframes.dtypes +# SQL Nodes are generally terminal, so don't support rich transformation methods +# like remap_vars, remap_refs, etc. +# Still, fields should be defined on them, as typing info is still used for +# dispatching some operators in the emitter, and for validation. + # TODO: Join node, union node @dataclasses.dataclass(frozen=True) @@ -84,6 +89,127 @@ def remap_refs( raise NotImplementedError() # type: ignore +@dataclasses.dataclass(frozen=True) +class SqlWithCtesNode(nodes.BigFrameNode): + # def, name pairs + child: nodes.BigFrameNode + cte_names: tuple[str, ...] + cte_defs: tuple[nodes.BigFrameNode, ...] + + @property + def child_nodes(self) -> Sequence[nodes.BigFrameNode]: + return (self.child, *self.cte_defs) + + @property + def fields(self) -> Sequence[nodes.Field]: + return self.child.fields + + @property + def variables_introduced(self) -> int: + # This operation only renames variables, doesn't actually create new ones + return 0 + + @property + def defines_namespace(self) -> bool: + return True + + @property + def explicitly_ordered(self) -> bool: + return False + + @property + def order_ambiguous(self) -> bool: + return True + + @property + def row_count(self) -> Optional[int]: + return self.child.row_count + + @property + def node_defined_ids(self) -> Tuple[identifiers.ColumnId, ...]: + return tuple(self.ids) + + @property + def consumed_ids(self): + return () + + @property + def _node_expressions(self): + return () + + def remap_vars( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> SqlWithCtesNode: + raise NotImplementedError() + + def remap_refs( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> SqlWithCtesNode: + raise NotImplementedError() # type: ignore + + def transform_children( + self, transform: Callable[[nodes.BigFrameNode], nodes.BigFrameNode] + ) -> SqlWithCtesNode: + return SqlWithCtesNode( + transform(self.child), + self.cte_names, + tuple(transform(cte) for cte in self.cte_defs), + ) + + +@dataclasses.dataclass(frozen=True) +class SqlCteRefNode(nodes.LeafNode): + cte_name: str + cte_schema: tuple[nodes.Field, ...] + + @property + def fields(self) -> Sequence[nodes.Field]: + return self.cte_schema + + @property + def variables_introduced(self) -> int: + # This operation only renames variables, doesn't actually create new ones + return 0 + + @property + def defines_namespace(self) -> bool: + return True + + @property + def explicitly_ordered(self) -> bool: + return False + + @property + def order_ambiguous(self) -> bool: + return True + + @property + def row_count(self) -> Optional[int]: + raise NotImplementedError() + + @property + def node_defined_ids(self) -> Tuple[identifiers.ColumnId, ...]: + return tuple(self.ids) + + @property + def consumed_ids(self): + return () + + @property + def _node_expressions(self): + return () + + def remap_vars( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> SqlCteRefNode: + raise NotImplementedError() + + def remap_refs( + self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId] + ) -> SqlCteRefNode: + raise NotImplementedError() # type: ignore + + @dataclasses.dataclass(frozen=True) class SqlSelectNode(nodes.UnaryNode): selections: tuple[nodes.ColumnDef, ...] = () diff --git a/bigframes/dtypes.py b/bigframes/dtypes.py index a2abe9b817a..304428ef2fa 100644 --- a/bigframes/dtypes.py +++ b/bigframes/dtypes.py @@ -118,6 +118,21 @@ ] LOCAL_SCALAR_TYPES = typing.get_args(LOCAL_SCALAR_TYPE) +SUPPORTED_LITERAL_TYPE = typing.Union[ + bytes, + str, + int, + bool, + float, + datetime.datetime, + datetime.date, + datetime.time, + decimal.Decimal, + list, + shapely.geometry.base.BaseGeometry, +] +SUPPORTED_LITERAL_TYPES = typing.get_args(SUPPORTED_LITERAL_TYPE) + # Will have a few dtype variants: simple(eg. int, string, bool), complex (eg. list, struct), and virtual (eg. micro intervals, categorical) @dataclass(frozen=True) @@ -709,10 +724,6 @@ def infer_literal_type(literal) -> typing.Optional[Dtype]: # Maybe also normalize literal to canonical python representation to remove this burden from compilers? if isinstance(literal, pa.Scalar): return arrow_dtype_to_bigframes_dtype(literal.type) - if pd.api.types.is_list_like(literal): - element_types = [infer_literal_type(i) for i in literal] - common_type = lcd_type(*element_types) - return list_type(common_type) if pd.api.types.is_dict_like(literal): fields = [] for key in literal.keys(): @@ -723,6 +734,10 @@ def infer_literal_type(literal) -> typing.Optional[Dtype]: pa.field(key, field_type, nullable=(not pa.types.is_list(field_type))) ) return pd.ArrowDtype(pa.struct(fields)) + if pd.api.types.is_list_like(literal): + element_types = [infer_literal_type(i) for i in literal] + common_type = lcd_type(*element_types) + return list_type(common_type) if pd.isna(literal): return None # Null value without a definite type # Make sure to check datetime before date as datetimes are also dates @@ -900,11 +915,16 @@ def is_compatible(scalar: typing.Any, dtype: Dtype) -> typing.Optional[Dtype]: def lcd_type(*dtypes: Dtype) -> Dtype: if len(dtypes) < 1: raise ValueError("at least one dypes should be provided") - if len(dtypes) == 1: - return dtypes[0] + unique_dtypes = set(dtypes) + if None in unique_dtypes: + unique_dtypes.remove(None) + + if len(unique_dtypes) == 0: + return None if len(unique_dtypes) == 1: - return unique_dtypes.pop() + return next(iter(unique_dtypes)) + # Implicit conversion currently only supported for numeric types hierarchy: list[Dtype] = [ BOOL_DTYPE, @@ -913,9 +933,9 @@ def lcd_type(*dtypes: Dtype) -> Dtype: BIGNUMERIC_DTYPE, FLOAT_DTYPE, ] - if any([dtype not in hierarchy for dtype in dtypes]): + if any([dtype not in hierarchy for dtype in unique_dtypes]): return None - lcd_index = max([hierarchy.index(dtype) for dtype in dtypes]) + lcd_index = max([hierarchy.index(dtype) for dtype in unique_dtypes]) return hierarchy[lcd_index] diff --git a/bigframes/extensions/__init__.py b/bigframes/extensions/__init__.py new file mode 100644 index 00000000000..58d482ea386 --- /dev/null +++ b/bigframes/extensions/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 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. diff --git a/bigframes/extensions/pandas/__init__.py b/bigframes/extensions/pandas/__init__.py new file mode 100644 index 00000000000..58d482ea386 --- /dev/null +++ b/bigframes/extensions/pandas/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 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. diff --git a/bigframes/extensions/pandas/dataframe_accessor.py b/bigframes/extensions/pandas/dataframe_accessor.py new file mode 100644 index 00000000000..2cb44fe3c5e --- /dev/null +++ b/bigframes/extensions/pandas/dataframe_accessor.py @@ -0,0 +1,67 @@ +# Copyright 2026 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 typing import cast + +import pandas +import pandas.api.extensions + +import bigframes.core.global_session as bf_session +import bigframes.pandas as bpd + + +@pandas.api.extensions.register_dataframe_accessor("bigquery") +class BigQueryDataFrameAccessor: + """ + Pandas DataFrame accessor for BigQuery DataFrames functionality. + + This accessor is registered under the ``bigquery`` namespace on pandas DataFrame objects. + """ + + def __init__(self, pandas_obj: pandas.DataFrame): + self._obj = pandas_obj + + def sql_scalar(self, sql_template: str, *, output_dtype=None, session=None): + """ + Compute a new pandas Series by applying a SQL scalar function to the DataFrame. + + The DataFrame is converted to BigFrames by calling ``read_pandas``, then the SQL + template is applied using ``bigframes.bigquery.sql_scalar``, and the result is + converted back to a pandas Series using ``to_pandas``. + + Args: + sql_template (str): + A SQL format string with Python-style {0}, {1}, etc. placeholders for each of + the columns in the DataFrame (in the order they appear in ``df.columns``). + output_dtype (a BigQuery DataFrames compatible dtype, optional): + If provided, BigQuery DataFrames uses this to determine the output + of the returned Series. This avoids a dry run query. + session (bigframes.session.Session, optional): + The BigFrames session to use. If not provided, the default global session is used. + + Returns: + pandas.Series: + The result of the SQL scalar function as a pandas Series. + """ + # Import bigframes.bigquery here to avoid circular imports + import bigframes.bigquery + + if session is None: + session = bf_session.get_global_session() + + bf_df = cast(bpd.DataFrame, session.read_pandas(self._obj)) + result = bigframes.bigquery.sql_scalar( + sql_template, bf_df, output_dtype=output_dtype + ) + return result.to_pandas(ordered=True) diff --git a/bigframes/functions/function.py b/bigframes/functions/function.py index 242daf7525d..4e06cb16633 100644 --- a/bigframes/functions/function.py +++ b/bigframes/functions/function.py @@ -214,10 +214,10 @@ def __call__(self, *args, **kwargs): if self._local_fun: return self._local_fun(*args, **kwargs) # avoid circular imports - import bigframes.core.sql as bf_sql + from bigframes.core.compile.sqlglot import sql as sg_sql import bigframes.session._io.bigquery as bf_io_bigquery - args_string = ", ".join(map(bf_sql.simple_literal, args)) + args_string = ", ".join([sg_sql.to_sql(sg_sql.literal(v)) for v in args]) sql = f"SELECT `{str(self._udf_def.routine_ref)}`({args_string})" iter, job = bf_io_bigquery.start_query_with_client( self._session.bqclient, @@ -298,10 +298,10 @@ def __call__(self, *args, **kwargs): if self._local_fun: return self._local_fun(*args, **kwargs) # avoid circular imports - import bigframes.core.sql as bf_sql + from bigframes.core.compile.sqlglot import sql as sg_sql import bigframes.session._io.bigquery as bf_io_bigquery - args_string = ", ".join(map(bf_sql.simple_literal, args)) + args_string = ", ".join([sg_sql.to_sql(sg_sql.literal(v)) for v in args]) sql = f"SELECT `{str(self._udf_def.routine_ref)}`({args_string})" iter, job = bf_io_bigquery.start_query_with_client( self._session.bqclient, diff --git a/bigframes/ml/compose.py b/bigframes/ml/compose.py index 9413cd06954..d81e3ab1bd6 100644 --- a/bigframes/ml/compose.py +++ b/bigframes/ml/compose.py @@ -27,7 +27,7 @@ import bigframes_vendored.sklearn.compose._column_transformer from google.cloud import bigquery -import bigframes.core.compile.sqlglot.sqlglot_ir as sql_utils +from bigframes.core.compile.sqlglot import sql as sg_sql from bigframes.core.logging import log_adapter import bigframes.core.utils as core_utils from bigframes.ml import base, core, globals, impute, preprocessing, utils @@ -111,9 +111,9 @@ def _compile_to_sql( columns, _ = core_utils.get_standardized_ids(columns) result = [] for column in columns: - current_sql = self._sql.format(sql_utils.identifier(column)) - current_target_column = sql_utils.identifier( - self._target_column.format(column) + current_sql = self._sql.format(sg_sql.to_sql(sg_sql.identifier(column))) + current_target_column = sg_sql.to_sql( + sg_sql.identifier(self._target_column.format(column)) ) result.append(f"{current_sql} AS {current_target_column}") return result diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index d90d23a4747..894fc44b1b3 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -21,8 +21,7 @@ import bigframes_vendored.constants as constants import google.cloud.bigquery -import bigframes.core.compile.sqlglot.sqlglot_ir as sql_utils -import bigframes.core.sql as sql_vals +from bigframes.core.compile.sqlglot import sql as sg_sql INDENT_STR = " " @@ -35,7 +34,7 @@ class BaseSqlGenerator: def encode_value(self, v: Union[str, int, float, Iterable[str]]) -> str: """Encode a parameter value for SQL""" if isinstance(v, (str, int, float)): - return sql_vals.simple_literal(v) + return sg_sql.to_sql(sg_sql.literal(v)) elif isinstance(v, Iterable): inner = ", ".join([self.encode_value(x) for x in v]) return f"[{inner}]" @@ -62,7 +61,7 @@ def build_structs(self, **kwargs: Union[int, float, str, Mapping]) -> str: 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)}" + f"{sg_sql.to_sql(sg_sql.literal(v_trans))} AS {sg_sql.to_sql(sg_sql.identifier(k))}" ) return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(param_strs) @@ -73,7 +72,9 @@ def build_expressions(self, *expr_sqls: str) -> str: def build_schema(self, **kwargs: str) -> str: """Encode a dict of values into a formatted schema type items for SQL""" - param_strs = [f"{sql_utils.identifier(k)} {v}" for k, v in kwargs.items()] + param_strs = [ + f"{sg_sql.to_sql(sg_sql.identifier(k))} {v}" for k, v in kwargs.items() + ] return "\n" + INDENT_STR + f",\n{INDENT_STR}".join(param_strs) def options(self, **kwargs: Union[str, int, float, Iterable[str]]) -> str: @@ -86,7 +87,9 @@ def struct_options(self, **kwargs: Union[int, float, Mapping]) -> str: def struct_columns(self, columns: Iterable[str]) -> str: """Encode a BQ Table columns to a STRUCT.""" - columns_str = ", ".join(map(sql_utils.identifier, columns)) + columns_str = ", ".join( + map(lambda x: sg_sql.to_sql(sg_sql.identifier(x)), columns) + ) return f"STRUCT({columns_str})" def input(self, **kwargs: str) -> str: @@ -109,15 +112,15 @@ def transform(self, *expr_sqls: str) -> str: def ml_standard_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.STANDARD_SCALER for BQML""" - return f"""ML.STANDARD_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.STANDARD_SCALER({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_max_abs_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MAX_ABS_SCALER for BQML""" - return f"""ML.MAX_ABS_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.MAX_ABS_SCALER({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_min_max_scaler(self, numeric_expr_sql: str, name: str) -> str: """Encode ML.MIN_MAX_SCALER for BQML""" - return f"""ML.MIN_MAX_SCALER({sql_utils.identifier(numeric_expr_sql)}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.MIN_MAX_SCALER({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_imputer( self, @@ -126,7 +129,7 @@ def ml_imputer( name: str, ) -> str: """Encode ML.IMPUTER for BQML""" - return f"""ML.IMPUTER({sql_utils.identifier(col_name)}, '{strategy}') OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.IMPUTER({sg_sql.to_sql(sg_sql.identifier(col_name))}, '{strategy}') OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_bucketize( self, @@ -140,7 +143,7 @@ def ml_bucketize( point.item() if hasattr(point, "item") else point for point in array_split_points ] - return f"""ML.BUCKETIZE({sql_utils.identifier(input_id)}, {points}, FALSE) AS {sql_utils.identifier(output_id)}""" + return f"""ML.BUCKETIZE({sg_sql.to_sql(sg_sql.identifier(input_id))}, {points}, FALSE) AS {sg_sql.to_sql(sg_sql.identifier(output_id))}""" def ml_quantile_bucketize( self, @@ -149,7 +152,7 @@ def ml_quantile_bucketize( name: str, ) -> str: """Encode ML.QUANTILE_BUCKETIZE for BQML""" - return f"""ML.QUANTILE_BUCKETIZE({sql_utils.identifier(numeric_expr_sql)}, {num_bucket}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.QUANTILE_BUCKETIZE({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}, {num_bucket}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_one_hot_encoder( self, @@ -162,7 +165,7 @@ def ml_one_hot_encoder( """Encode ML.ONE_HOT_ENCODER for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-one-hot-encoder for params. """ - return f"""ML.ONE_HOT_ENCODER({sql_utils.identifier(numeric_expr_sql)}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.ONE_HOT_ENCODER({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}, '{drop}', {top_k}, {frequency_threshold}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_label_encoder( self, @@ -174,7 +177,7 @@ def ml_label_encoder( """Encode ML.LABEL_ENCODER for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-label-encoder for params. """ - return f"""ML.LABEL_ENCODER({sql_utils.identifier(numeric_expr_sql)}, {top_k}, {frequency_threshold}) OVER() AS {sql_utils.identifier(name)}""" + return f"""ML.LABEL_ENCODER({sg_sql.to_sql(sg_sql.identifier(numeric_expr_sql))}, {top_k}, {frequency_threshold}) OVER() AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_polynomial_expand( self, columns: Iterable[str], degree: int, name: str @@ -182,7 +185,7 @@ def ml_polynomial_expand( """Encode ML.POLYNOMIAL_EXPAND. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-polynomial-expand """ - return f"""ML.POLYNOMIAL_EXPAND({self.struct_columns(columns)}, {degree}) AS {sql_utils.identifier(name)}""" + return f"""ML.POLYNOMIAL_EXPAND({self.struct_columns(columns)}, {degree}) AS {sg_sql.to_sql(sg_sql.identifier(name))}""" def ml_distance( self, @@ -195,7 +198,7 @@ def ml_distance( """Encode ML.DISTANCE for BQML. https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-distance """ - return f"""SELECT *, ML.DISTANCE({sql_utils.identifier(col_x)}, {sql_utils.identifier(col_y)}, '{type}') AS {sql_utils.identifier(name)} FROM ({source_sql})""" + return f"""SELECT *, ML.DISTANCE({sg_sql.to_sql(sg_sql.identifier(col_x))}, {sg_sql.to_sql(sg_sql.identifier(col_y))}, '{type}') AS {sg_sql.to_sql(sg_sql.identifier(name))} FROM ({source_sql})""" def ai_forecast( self, @@ -217,7 +220,7 @@ def _model_id_sql( self, model_ref: google.cloud.bigquery.ModelReference, ): - return f"{sql_utils.identifier(model_ref.project)}.{sql_utils.identifier(model_ref.dataset_id)}.{sql_utils.identifier(model_ref.model_id)}" + return f"{sg_sql.to_sql(sg_sql.identifier(model_ref.project))}.{sg_sql.to_sql(sg_sql.identifier(model_ref.dataset_id))}.{sg_sql.to_sql(sg_sql.identifier(model_ref.model_id))}" # Model create and alter def create_model( @@ -308,7 +311,7 @@ def __init__(self, model_ref: google.cloud.bigquery.ModelReference): self._model_ref = model_ref def _model_ref_sql(self) -> str: - return f"{sql_utils.identifier(self._model_ref.project)}.{sql_utils.identifier(self._model_ref.dataset_id)}.{sql_utils.identifier(self._model_ref.model_id)}" + return f"{sg_sql.to_sql(sg_sql.identifier(self._model_ref.project))}.{sg_sql.to_sql(sg_sql.identifier(self._model_ref.dataset_id))}.{sg_sql.to_sql(sg_sql.identifier(self._model_ref.model_id))}" # Alter model def alter_model( diff --git a/bigframes/operations/ai_ops.py b/bigframes/operations/ai_ops.py index 8dc8c2ffab4..b20314fe232 100644 --- a/bigframes/operations/ai_ops.py +++ b/bigframes/operations/ai_ops.py @@ -123,7 +123,7 @@ class AIIf(base_ops.NaryOp): name: ClassVar[str] = "ai_if" prompt_context: Tuple[str | None, ...] - connection_id: str + connection_id: str | None def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return dtypes.BOOL_DTYPE @@ -135,7 +135,7 @@ class AIClassify(base_ops.NaryOp): prompt_context: Tuple[str | None, ...] categories: tuple[str, ...] - connection_id: str + connection_id: str | None def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return dtypes.STRING_DTYPE @@ -146,7 +146,7 @@ class AIScore(base_ops.NaryOp): name: ClassVar[str] = "ai_score" prompt_context: Tuple[str | None, ...] - connection_id: str + connection_id: str | None def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return dtypes.FLOAT_DTYPE diff --git a/bigframes/operations/datetime_ops.py b/bigframes/operations/datetime_ops.py index 19541a383c8..37a6035ef78 100644 --- a/bigframes/operations/datetime_ops.py +++ b/bigframes/operations/datetime_ops.py @@ -74,6 +74,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT dtypes.STRING_DTYPE, dtypes.DATE_DTYPE, dtypes.TIMESTAMP_DTYPE, + dtypes.DATETIME_DTYPE, ): raise TypeError("expected string or numeric input") return pd.ArrowDtype(pa.timestamp("us", tz=None)) @@ -87,6 +88,8 @@ class ToTimestampOp(base_ops.UnaryOp): def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: # Must be numeric or string + if input_types[0] == dtypes.TIMESTAMP_DTYPE: + raise TypeError("Already tz-aware.") if input_types[0] not in ( dtypes.FLOAT_DTYPE, dtypes.INT_DTYPE, diff --git a/bigframes/operations/datetimes.py b/bigframes/operations/datetimes.py index f66c37bb645..2850919ee31 100644 --- a/bigframes/operations/datetimes.py +++ b/bigframes/operations/datetimes.py @@ -15,13 +15,16 @@ from __future__ import annotations import datetime as dt -from typing import Literal, Optional +from typing import Generic, Literal, Optional, TypeVar import bigframes_vendored.pandas.core.arrays.datetimelike as vendored_pandas_datetimelike import bigframes_vendored.pandas.core.indexes.accessor as vendordt import pandas from bigframes import dataframe, dtypes, series +from bigframes._tools import docs +import bigframes.core.col +import bigframes.core.indexes.base as indices from bigframes.core.logging import log_adapter import bigframes.operations as ops @@ -31,152 +34,156 @@ _SUPPORTED_FREQS = ("Y", "Q", "M", "W", "D", "h", "min", "s", "ms", "us") -@log_adapter.class_logger -class DatetimeMethods( - vendordt.DatetimeProperties, - vendored_pandas_datetimelike.DatelikeOps, -): - __doc__ = vendordt.DatetimeProperties.__doc__ +T = TypeVar("T", series.Series, indices.Index, bigframes.core.col.Expression) - def __init__(self, data: series.Series): - self._data = data + +# Simpler base class for datetime properties, excludes isocalendar, unit, tz +class DatetimeSimpleMethods(Generic[T]): + def __init__(self, data: T): + self._data: T = data # Date accessors @property - def day(self) -> series.Series: + def day(self) -> T: return self._data._apply_unary_op(ops.day_op) @property - def dayofweek(self) -> series.Series: + def dayofweek(self) -> T: return self._data._apply_unary_op(ops.dayofweek_op) @property - def day_of_week(self) -> series.Series: + def day_of_week(self) -> T: return self.dayofweek @property - def weekday(self) -> series.Series: + def weekday(self) -> T: return self.dayofweek @property - def dayofyear(self) -> series.Series: + def dayofyear(self) -> T: return self._data._apply_unary_op(ops.dayofyear_op) @property - def day_of_year(self) -> series.Series: + def day_of_year(self) -> T: return self.dayofyear @property - def date(self) -> series.Series: + def date(self) -> T: return self._data._apply_unary_op(ops.date_op) @property - def quarter(self) -> series.Series: + def quarter(self) -> T: return self._data._apply_unary_op(ops.quarter_op) @property - def year(self) -> series.Series: + def year(self) -> T: return self._data._apply_unary_op(ops.year_op) @property - def month(self) -> series.Series: + def month(self) -> T: return self._data._apply_unary_op(ops.month_op) - def isocalendar(self) -> dataframe.DataFrame: - iso_ops = [ops.iso_year_op, ops.iso_week_op, ops.iso_day_op] - labels = pandas.Index(["year", "week", "day"]) - block = self._data._block.project_exprs( - [op.as_expr(self._data._value_column) for op in iso_ops], labels, drop=True - ) - return dataframe.DataFrame(block) - # Time accessors @property - def hour(self) -> series.Series: + def hour(self) -> T: return self._data._apply_unary_op(ops.hour_op) @property - def minute(self) -> series.Series: + def minute(self) -> T: return self._data._apply_unary_op(ops.minute_op) @property - def second(self) -> series.Series: + def second(self) -> T: return self._data._apply_unary_op(ops.second_op) @property - def time(self) -> series.Series: + def time(self) -> T: return self._data._apply_unary_op(ops.time_op) # Timedelta accessors @property - def days(self) -> series.Series: + def days(self) -> T: self._check_dtype(dtypes.TIMEDELTA_DTYPE) return self._data._apply_binary_op(_ONE_DAY, ops.floordiv_op) @property - def seconds(self) -> series.Series: + def seconds(self) -> T: self._check_dtype(dtypes.TIMEDELTA_DTYPE) return self._data._apply_binary_op(_ONE_DAY, ops.mod_op) // _ONE_SECOND # type: ignore @property - def microseconds(self) -> series.Series: + def microseconds(self) -> T: self._check_dtype(dtypes.TIMEDELTA_DTYPE) return self._data._apply_binary_op(_ONE_SECOND, ops.mod_op) // _ONE_MICRO # type: ignore - def total_seconds(self) -> series.Series: + def total_seconds(self) -> T: self._check_dtype(dtypes.TIMEDELTA_DTYPE) return self._data._apply_binary_op(_ONE_SECOND, ops.div_op) def _check_dtype(self, target_dtype: dtypes.Dtype): - if self._data._dtype == target_dtype: - return - raise TypeError(f"Expect dtype: {target_dtype}, but got {self._data._dtype}") - - @property - def tz(self) -> Optional[dt.timezone]: - # Assumption: pyarrow dtype - tz_string = self._data._dtype.pyarrow_dtype.tz - if tz_string == "UTC": - return dt.timezone.utc - elif tz_string is None: - return None - else: - raise ValueError(f"Unexpected timezone {tz_string}") - - def tz_localize(self, tz: Literal["UTC"] | None) -> series.Series: + if isinstance(self._data, (indices.Index, series.Series)): + if self._data.dtype != target_dtype: + raise TypeError( + f"Expect dtype: {target_dtype}, but got {self._data.dtype}" + ) + return + + def tz_localize(self, tz: Literal["UTC"] | None) -> T: if tz == "UTC": - if self._data.dtype == dtypes.TIMESTAMP_DTYPE: - raise ValueError("Already tz-aware.") - return self._data._apply_unary_op(ops.ToTimestampOp()) if tz is None: - if self._data.dtype == dtypes.DATETIME_DTYPE: - return self._data # no-op - return self._data._apply_unary_op(ops.ToDatetimeOp()) raise ValueError(f"Unsupported timezone {tz}") - @property - def unit(self) -> str: - # Assumption: pyarrow dtype - return self._data._dtype.pyarrow_dtype.unit - - def day_name(self) -> series.Series: + def day_name(self) -> T: return self.strftime("%A") - def strftime(self, date_format: str) -> series.Series: + def strftime(self, date_format: str) -> T: return self._data._apply_unary_op(ops.StrftimeOp(date_format=date_format)) - def normalize(self) -> series.Series: + def normalize(self) -> T: return self._data._apply_unary_op(ops.normalize_op) - def floor(self, freq: str) -> series.Series: + def floor(self, freq: str) -> T: if freq not in _SUPPORTED_FREQS: raise ValueError(f"freq must be one of {_SUPPORTED_FREQS}") return self._data._apply_unary_op(ops.FloorDtOp(freq=freq)) # type: ignore + + +# this is the version used by series.dt, and the one that shows up in reference docs +@log_adapter.class_logger +@docs.inherit_docs(vendordt.DatetimeProperties) +@docs.inherit_docs(vendored_pandas_datetimelike.DatelikeOps) +class DatetimeMethods(DatetimeSimpleMethods[bigframes.series.Series]): + def __init__(self, data: series.Series): + super().__init__(data) + + @property + def tz(self) -> Optional[dt.timezone]: + # Assumption: pyarrow dtype + tz_string = self._data._dtype.pyarrow_dtype.tz + if tz_string == "UTC": + return dt.timezone.utc + elif tz_string is None: + return None + else: + raise ValueError(f"Unexpected timezone {tz_string}") + + @property + def unit(self) -> str: + # Assumption: pyarrow dtype + return self._data._dtype.pyarrow_dtype.unit + + def isocalendar(self) -> dataframe.DataFrame: + iso_ops = [ops.iso_year_op, ops.iso_week_op, ops.iso_day_op] + labels = pandas.Index(["year", "week", "day"]) + block = self._data._block.project_exprs( + [op.as_expr(self._data._value_column) for op in iso_ops], labels, drop=True + ) + return dataframe.DataFrame(block) diff --git a/bigframes/operations/generic_ops.py b/bigframes/operations/generic_ops.py index d6155a770c1..f7175ec2793 100644 --- a/bigframes/operations/generic_ops.py +++ b/bigframes/operations/generic_ops.py @@ -443,10 +443,15 @@ class SqlScalarOp(base_ops.NaryOp): name: typing.ClassVar[str] = "sql_scalar" _output_type: dtypes.ExpressionType sql_template: str + is_deterministic: bool = True def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: return self._output_type + @property + def deterministic(self) -> bool: + return self.is_deterministic + @dataclasses.dataclass(frozen=True) class PyUdfOp(base_ops.NaryOp): diff --git a/bigframes/operations/strings.py b/bigframes/operations/strings.py index 8b5b57b259e..1712d423557 100644 --- a/bigframes/operations/strings.py +++ b/bigframes/operations/strings.py @@ -21,6 +21,7 @@ import bigframes_vendored.pandas.core.strings.accessor as vendorstr from bigframes._tools import docs +import bigframes.core.col import bigframes.core.indexes.base as indices from bigframes.core.logging import log_adapter import bigframes.dataframe as df @@ -36,7 +37,7 @@ re.DOTALL: "s", } -T = TypeVar("T", series.Series, indices.Index) +T = TypeVar("T", series.Series, indices.Index, bigframes.core.col.Expression) @log_adapter.class_logger @@ -324,7 +325,14 @@ def to_blob(self, connection: Optional[str] = None) -> T: bigframes.series.Series: Blob Series. """ - session = self._data._block.session + import bigframes.core.blocks + + if hasattr(self._data, "_block") and isinstance( + self._data._block, bigframes.core.blocks.Block + ): + session = self._data._block.session + else: + raise ValueError("to_blob is only supported via Series.str") connection = session._create_bq_connection(connection=connection) return self._data._apply_binary_op(connection, ops.obj_make_ref_op) diff --git a/bigframes/pandas/core/methods/describe.py b/bigframes/pandas/core/methods/describe.py index 6fd7960daf3..34c116ba27d 100644 --- a/bigframes/pandas/core/methods/describe.py +++ b/bigframes/pandas/core/methods/describe.py @@ -56,9 +56,10 @@ def describe( "max", ] ).intersection(describe_block.column_labels.get_level_values(-1)) - describe_block = describe_block.stack(override_labels=stack_cols) - - return dataframe.DataFrame(describe_block).droplevel(level=0) + if not stack_cols.empty: + describe_block = describe_block.stack(override_labels=stack_cols) + return dataframe.DataFrame(describe_block).droplevel(level=0) + return dataframe.DataFrame(describe_block) def _describe( @@ -120,5 +121,7 @@ def _get_aggs_for_dtype(dtype) -> list[aggregations.UnaryAggregateOp]: dtypes.TIME_DTYPE, ]: return [aggregations.count_op, aggregations.nunique_op] + elif dtypes.is_json_like(dtype) or dtype == dtypes.OBJ_REF_DTYPE: + return [aggregations.count_op] else: return [] diff --git a/bigframes/series.py b/bigframes/series.py index 2d0b13b4700..23799a0a43c 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -73,7 +73,6 @@ import bigframes.operations as ops import bigframes.operations.aggregations as agg_ops import bigframes.operations.blob as blob -import bigframes.operations.datetimes as dt import bigframes.operations.lists as lists import bigframes.operations.plotting as plotting import bigframes.operations.python_op_maps as python_ops @@ -82,6 +81,7 @@ if typing.TYPE_CHECKING: import bigframes.geopandas.geoseries + import bigframes.operations.datetimes as datetimes import bigframes.operations.strings as strings @@ -208,8 +208,10 @@ def __init__( self._block.session._register_object(self) @property - def dt(self) -> dt.DatetimeMethods: - return dt.DatetimeMethods(self) + def dt(self) -> datetimes.DatetimeMethods: + import bigframes.operations.datetimes as datetimes + + return datetimes.DatetimeMethods(self) @property def dtype(self): diff --git a/bigframes/session/_io/bigquery/__init__.py b/bigframes/session/_io/bigquery/__init__.py index 1d1dc57c30e..61b22d03115 100644 --- a/bigframes/session/_io/bigquery/__init__.py +++ b/bigframes/session/_io/bigquery/__init__.py @@ -32,7 +32,7 @@ import google.cloud.bigquery._job_helpers import google.cloud.bigquery.table -from bigframes.core.compile.sqlglot import sqlglot_ir +from bigframes.core.compile.sqlglot import sql as sg_sql import bigframes.core.events from bigframes.core.logging import log_adapter import bigframes.core.sql @@ -534,12 +534,12 @@ def to_query( time_travel_clause = "" if time_travel_timestamp is not None: - time_travel_literal = bigframes.core.sql.simple_literal(time_travel_timestamp) + time_travel_literal = sg_sql.to_sql(sg_sql.literal(time_travel_timestamp)) time_travel_clause = f" FOR SYSTEM_TIME AS OF {time_travel_literal}" limit_clause = "" if max_results is not None: - limit_clause = f" LIMIT {bigframes.core.sql.simple_literal(max_results)}" + limit_clause = f" LIMIT {sg_sql.to_sql(sg_sql.literal(max_results))}" where_clause = f" WHERE {sql_predicate}" if sql_predicate else "" @@ -599,11 +599,11 @@ def compile_filters(filters: third_party_pandas_gbq.FiltersType) -> str: operator_str = valid_operators[operator] - column_ref = sqlglot_ir.identifier(column) + column_ref = sg_sql.to_sql(sg_sql.identifier(column)) if operator_str in ["IN", "NOT IN"]: value_literal = bigframes.core.sql.multi_literal(*value) else: - value_literal = bigframes.core.sql.simple_literal(value) + value_literal = sg_sql.to_sql(sg_sql.literal(value)) expression = bigframes.core.sql.infix_op( operator_str, column_ref, value_literal ) diff --git a/bigframes/session/bigquery_session.py b/bigframes/session/bigquery_session.py index 1a38bca1e82..79fb21486c7 100644 --- a/bigframes/session/bigquery_session.py +++ b/bigframes/session/bigquery_session.py @@ -24,7 +24,7 @@ import bigframes_vendored.ibis.backends.bigquery.datatypes as ibis_bq import google.cloud.bigquery as bigquery -from bigframes.core.compile.sqlglot import sqlglot_ir +from bigframes.core.compile.sqlglot import sql as sg_sql import bigframes.core.events from bigframes.session import temporary_storage import bigframes.session._io.bigquery as bfbqio @@ -80,7 +80,7 @@ def create_temp_table( ibis_schema = ibis_bq.BigQuerySchema.to_ibis(list(schema)) fields = [ - f"{sqlglot_ir.identifier(name)} {ibis_bq.BigQueryType.from_ibis(ibis_type)}" + f"{sg_sql.to_sql(sg_sql.identifier(name))} {ibis_bq.BigQueryType.from_ibis(ibis_type)}" for name, ibis_type in ibis_schema.fields.items() ] fields_string = ",".join(fields) @@ -88,12 +88,12 @@ def create_temp_table( cluster_string = "" if cluster_cols: cluster_cols_sql = ", ".join( - f"{sqlglot_ir.identifier(cluster_col)}" + f"{sg_sql.to_sql(sg_sql.identifier(cluster_col))}" for cluster_col in cluster_cols ) cluster_string = f"\nCLUSTER BY {cluster_cols_sql}" - ddl = f"CREATE TEMP TABLE `_SESSION`.{sqlglot_ir.identifier(table_ref.table_id)} ({fields_string}){cluster_string}" + ddl = f"CREATE TEMP TABLE `_SESSION`.{sg_sql.to_sql(sg_sql.identifier(table_ref.table_id))} ({fields_string}){cluster_string}" _, job = bfbqio.start_query_with_client( self.bqclient, diff --git a/bigframes/session/bq_caching_executor.py b/bigframes/session/bq_caching_executor.py index a5c01765655..1e240a841c5 100644 --- a/bigframes/session/bq_caching_executor.py +++ b/bigframes/session/bq_caching_executor.py @@ -30,6 +30,7 @@ import bigframes.constants import bigframes.core from bigframes.core import bq_data, compile, local_data, rewrite +from bigframes.core.compile.sqlglot import sql as sg_sql from bigframes.core.compile.sqlglot import sqlglot_ir import bigframes.core.events import bigframes.core.guid @@ -304,12 +305,13 @@ def _export_gbq( # BigQuery `RATE_LIMIT_EXCEEDED` errors, as per quota limits: # https://cloud.google.com/bigquery/quotas#standard_tables job_config = bigquery.QueryJobConfig() - ir = sqlglot_ir.SQLGlotIR.from_query_string(sql) + + ir = sqlglot_ir.SQLGlotIR.from_unparsed_query(sql) if spec.if_exists == "append": - sql = ir.insert(spec.table) + sql = sg_sql.to_sql(sg_sql.insert(ir.expr.as_select_all(), spec.table)) else: # for "replace" assert spec.if_exists == "replace" - sql = ir.replace(spec.table) + sql = sg_sql.to_sql(sg_sql.replace(ir.expr.as_select_all(), spec.table)) else: dispositions = { "fail": bigquery.WriteDisposition.WRITE_EMPTY, diff --git a/bigframes/testing/__init__.py b/bigframes/testing/__init__.py index 9c1fb7c283b..098a67bddf3 100644 --- a/bigframes/testing/__init__.py +++ b/bigframes/testing/__init__.py @@ -17,10 +17,5 @@ These modules are provided for testing the BigQuery DataFrames package. The interface is not considered stable. """ -from bigframes.testing.utils import ( - assert_frame_equal, - assert_index_equal, - assert_series_equal, -) -__all__ = ["assert_frame_equal", "assert_series_equal", "assert_index_equal"] +# Do not import modules contains pytest. (b/490160312) diff --git a/bigframes/version.py b/bigframes/version.py index 012a4502914..4928dd5c209 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.37.0" +__version__ = "2.38.0" # {x-release-please-start-date} -__release_date__ = "2026-03-03" +__release_date__ = "2026-03-16" # {x-release-please-end} diff --git a/docs/conf.py b/docs/conf.py index b4954ac6592..b518ac074fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,9 +59,12 @@ "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx_sitemap", - "myst_parser", + "myst_nb", ] +# myst-nb configuration +nb_execution_mode = "off" + # autodoc/autosummary flags autoclass_content = "both" autodoc_default_options = {"members": True} @@ -269,12 +272,16 @@ suppress_warnings = [ + # Allow unknown mimetype so we can use widgets in tutorial notebooks. + "mystnb.unknown_mime_type", # Temporarily suppress this to avoid "more than one target found for # cross-reference" warning, which are intractable for us to avoid while in # a mono-repo. # See https://github.com/sphinx-doc/sphinx/blob # /2a65ffeef5c107c19084fabdd706cdff3f52d93c/sphinx/domains/python.py#L843 - "ref.python" + "ref.python", + # Allow external websites to be down occasionally. + "intersphinx.external", ] # -- Options for LaTeX output --------------------------------------------- @@ -383,7 +390,8 @@ "grpc": ("https://grpc.github.io/grpc/python/", None), "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), "protobuf": ("https://googleapis.dev/python/protobuf/latest/", None), - "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + # TODO(tswast): re-enable if we can get temporary failures to be ignored. + # "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), "pydata-google-auth": ( "https://pydata-google-auth.readthedocs.io/en/latest/", None, diff --git a/docs/notebooks b/docs/notebooks new file mode 120000 index 00000000000..8f9a5b2e6d2 --- /dev/null +++ b/docs/notebooks @@ -0,0 +1 @@ +../notebooks \ No newline at end of file diff --git a/docs/reference/index.rst b/docs/reference/index.rst index bdf38e977da..cb295a43099 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -19,6 +19,16 @@ packages. bigframes.pandas.api.typing bigframes.streaming +Pandas Extensions +~~~~~~~~~~~~~~~~~ + +BigQuery DataFrames provides extensions to pandas DataFrame objects. + +.. autosummary:: + :toctree: api + + bigframes.extensions.pandas.dataframe_accessor.BigQueryDataFrameAccessor + ML APIs ~~~~~~~ diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst index 18644829b33..af09616e055 100644 --- a/docs/user_guide/index.rst +++ b/docs/user_guide/index.rst @@ -9,3 +9,117 @@ User Guide Getting Started Cloud Docs User Guides + +.. toctree:: + :caption: Getting Started + :maxdepth: 1 + + Quickstart Template <../notebooks/getting_started/bq_dataframes_template.ipynb> + Getting Started <../notebooks/getting_started/getting_started_bq_dataframes.ipynb> + Magics <../notebooks/getting_started/magics.ipynb> + ML Fundamentals <../notebooks/getting_started/ml_fundamentals_bq_dataframes.ipynb> + Pandas Extensions <../notebooks/getting_started/pandas_extensions.ipynb> + +.. toctree:: + :caption: DataFrames + :maxdepth: 1 + + Anywidget Mode <../notebooks/dataframes/anywidget_mode.ipynb> + Dataframe <../notebooks/dataframes/dataframe.ipynb> + Index Col Null <../notebooks/dataframes/index_col_null.ipynb> + Integrations <../notebooks/dataframes/integrations.ipynb> + Pypi <../notebooks/dataframes/pypi.ipynb> + +.. toctree:: + :caption: Data Types + :maxdepth: 1 + + Array <../notebooks/data_types/array.ipynb> + Json <../notebooks/data_types/json.ipynb> + Struct <../notebooks/data_types/struct.ipynb> + Timedelta <../notebooks/data_types/timedelta.ipynb> + +.. toctree:: + :caption: Generative AI + :maxdepth: 1 + + AI Functions <../notebooks/generative_ai/ai_functions.ipynb> + AI Forecast <../notebooks/generative_ai/bq_dataframes_ai_forecast.ipynb> + LLM Code Generation <../notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb> + LLM KMeans <../notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb> + LLM Output Schema <../notebooks/generative_ai/bq_dataframes_llm_output_schema.ipynb> + LLM Vector Search <../notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb> + Drug Name Generation <../notebooks/generative_ai/bq_dataframes_ml_drug_name_generation.ipynb> + Large Language Models <../notebooks/generative_ai/large_language_models.ipynb> + +.. toctree:: + :caption: Machine Learning + :maxdepth: 1 + + ML Cross Validation <../notebooks/ml/bq_dataframes_ml_cross_validation.ipynb> + Linear Regression <../notebooks/ml/bq_dataframes_ml_linear_regression.ipynb> + Linear Regression BBQ <../notebooks/ml/bq_dataframes_ml_linear_regression_bbq.ipynb> + Linear Regression Big <../notebooks/ml/bq_dataframes_ml_linear_regression_big.ipynb> + Easy Linear Regression <../notebooks/ml/easy_linear_regression.ipynb> + Sklearn Linear Regression <../notebooks/ml/sklearn_linear_regression.ipynb> + Timeseries Analysis <../notebooks/ml/timeseries_analysis.ipynb> + +.. toctree:: + :caption: Visualization + :maxdepth: 1 + + COVID Line Graphs <../notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb> + Tutorial <../notebooks/visualization/tutorial.ipynb> + +.. toctree:: + :caption: Geospatial Data + :maxdepth: 1 + + Geoseries <../notebooks/geo/geoseries.ipynb> + +.. toctree:: + :caption: Regionalized BigQuery + :maxdepth: 1 + + Regionalized <../notebooks/location/regionalized.ipynb> + +.. toctree:: + :caption: Multimodal + :maxdepth: 1 + + Multimodal Dataframe <../notebooks/multimodal/multimodal_dataframe.ipynb> + +.. toctree:: + :caption: Remote Functions + :maxdepth: 1 + + Remote Function <../notebooks/remote_functions/remote_function.ipynb> + Remote Function Usecases <../notebooks/remote_functions/remote_function_usecases.ipynb> + Remote Function Vertex Claude Model <../notebooks/remote_functions/remote_function_vertex_claude_model.ipynb> + +.. toctree:: + :caption: Streaming + :maxdepth: 1 + + Streaming Dataframe <../notebooks/streaming/streaming_dataframe.ipynb> + +.. toctree:: + :caption: Experimental + :maxdepth: 1 + + AI Operators <../notebooks/experimental/ai_operators.ipynb> + Semantic Operators <../notebooks/experimental/semantic_operators.ipynb> + +.. toctree:: + :caption: Apps + :maxdepth: 1 + + Synthetic Data Generation <../notebooks/apps/synthetic_data_generation.ipynb> + +.. toctree:: + :caption: Kaggle + :maxdepth: 1 + + AI Forecast <../notebooks/kaggle/bq_dataframes_ai_forecast.ipynb> + Describe Product Images <../notebooks/kaggle/describe-product-images-with-bigframes-multimodal.ipynb> + Vector Search Over National Jukebox <../notebooks/kaggle/vector-search-with-bigframes-over-national-jukebox.ipynb> diff --git a/notebooks/dataframes/dataframe.ipynb b/notebooks/dataframes/dataframe.ipynb index de9bb1d04f4..f26b4ff1cf1 100644 --- a/notebooks/dataframes/dataframe.ipynb +++ b/notebooks/dataframes/dataframe.ipynb @@ -49,7 +49,7 @@ "id": "13861abc-120c-4db6-ad0c-e414b85d3443", "metadata": {}, "source": [ - "### Select a subset of the DF" + "## Select a subset of the DF" ] }, { diff --git a/notebooks/dataframes/index_col_null.ipynb b/notebooks/dataframes/index_col_null.ipynb index 655745dd2be..f77051e553b 100644 --- a/notebooks/dataframes/index_col_null.ipynb +++ b/notebooks/dataframes/index_col_null.ipynb @@ -358,7 +358,7 @@ "id": "13861abc-120c-4db6-ad0c-e414b85d3443", "metadata": {}, "source": [ - "### Select a subset of the DataFrame\n", + "## Select a subset of the DataFrame", "\n", "Filter columns by selecting a list of columns from the DataFrame.\n", "\n", diff --git a/notebooks/experimental/ai_operators.ipynb b/notebooks/experimental/ai_operators.ipynb index e24ec34d86d..e054484a0bf 100644 --- a/notebooks/experimental/ai_operators.ipynb +++ b/notebooks/experimental/ai_operators.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# AI Operators (Experimental)" + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/notebooks/experimental/semantic_operators.ipynb b/notebooks/experimental/semantic_operators.ipynb index c32ac9042b8..22927e6ef94 100644 --- a/notebooks/experimental/semantic_operators.ipynb +++ b/notebooks/experimental/semantic_operators.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# Semantic Operators (Experimental)" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/notebooks/generative_ai/bq_dataframes_ai_forecast.ipynb b/notebooks/generative_ai/bq_dataframes_ai_forecast.ipynb index b9599282b38..6f8c95d3a48 100644 --- a/notebooks/generative_ai/bq_dataframes_ai_forecast.ipynb +++ b/notebooks/generative_ai/bq_dataframes_ai_forecast.ipynb @@ -60,7 +60,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Setup" + "## Setup" ] }, { diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index b05a6f034f6..c2c85be2b0c 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -1,1305 +1,1305 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2022 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": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "## Use BigQuery DataFrames with Generative AI for code generation\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "24743cf4a1e1" - }, - "source": [ - "**_NOTE_**: This notebook has been tested in the following environment:\n", - "\n", - "* Python version = 3.10" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "Use this notebook to walk through an example use case of generating sample code by using BigQuery DataFrames and its integration with Generative AI support on Vertex AI.\n", - "\n", - "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you create a CSV file containing sample code for calling a given set of APIs.\n", - "\n", - "The steps include:\n", - "\n", - "- Defining an LLM model in BigQuery DataFrames, specifically the [Gemini Model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models), using `bigframes.ml.llm`.\n", - "- Creating a DataFrame by reading in data from Cloud Storage.\n", - "- Manipulating data in the DataFrame to build LLM prompts.\n", - "- Sending DataFrame prompts to the LLM model using the `predict` method.\n", - "- Creating and using a custom function to transform the output provided by the LLM model response.\n", - "- Exporting the resulting transformed DataFrame as a CSV file." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses a dataset listing the names of various pandas DataFrame and Series APIs." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery\n", - "* Generative AI support on Vertex AI\n", - "* Cloud Functions\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", - "[Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models), and [Cloud Functions pricing](https://cloud.google.com/functions/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "Install the following packages, which are required to run this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "2b4ef9b72d43" - }, - "outputs": [], - "source": [ - "!pip install bigframes --upgrade --quiet" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Wbr2aVtFQBcg" - }, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", - "\n", - " * BigQuery API\n", - " * BigQuery Connection API\n", - " * Cloud Functions API\n", - " * Cloud Run API\n", - " * Artifact Registry API\n", - " * Cloud Build API\n", - " * Cloud Resource Manager API\n", - " * Vertex AI API\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2022 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": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Use BigQuery DataFrames with Generative AI for code generation", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to walk through an example use case of generating sample code by using BigQuery DataFrames and its integration with Generative AI support on Vertex AI.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you create a CSV file containing sample code for calling a given set of APIs.\n", + "\n", + "The steps include:\n", + "\n", + "- Defining an LLM model in BigQuery DataFrames, specifically the [Gemini Model](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models), using `bigframes.ml.llm`.\n", + "- Creating a DataFrame by reading in data from Cloud Storage.\n", + "- Manipulating data in the DataFrame to build LLM prompts.\n", + "- Sending DataFrame prompts to the LLM model using the `predict` method.\n", + "- Creating and using a custom function to transform the output provided by the LLM model response.\n", + "- Exporting the resulting transformed DataFrame as a CSV file." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses a dataset listing the names of various pandas DataFrame and Series APIs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery\n", + "* Generative AI support on Vertex AI\n", + "* Cloud Functions\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "[Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models), and [Cloud Functions pricing](https://cloud.google.com/functions/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages, which are required to run this notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "!pip install bigframes --upgrade --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wbr2aVtFQBcg" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com,artifactregistry.googleapis.com,cloudbuild.googleapis.com,cloudresourcemanager.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Cloud Functions API\n", + " * Cloud Run API\n", + " * Artifact Registry API\n", + " * Cloud Build API\n", + " * Cloud Resource Manager API\n", + " * Vertex AI API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "If you don't know your project ID, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1;31mERROR:\u001b[0m (gcloud.config.set) argument VALUE: Must be specified.\n", + "Usage: gcloud config set SECTION/PROPERTY VALUE [optional flags]\n", + " optional flags may be --help | --installation\n", + "\n", + "For detailed information on this command and its flags, run:\n", + " gcloud config set --help\n" + ] + } + ], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "from google.cloud import bigquery\n", + "from google.cloud import bigquery_connection_v1 as bq_connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bf.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DTVtFlqeFbrU" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6eytf4xQHzcF" + }, + "source": [ + "# Define the LLM model\n", + "\n", + "BigQuery DataFrames provides integration with [Gemini Models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) via Vertex AI.\n", + "\n", + "This section walks through a few steps required in order to use the model in your notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qUjT8nw-jIXp" + }, + "source": [ + "## Define the model\n", + "\n", + "Use `bigframes.ml.llm` to define the model:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "sdjeXFwcHfl7" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1;31mERROR:\u001b[0m (gcloud.config.set) argument VALUE: Must be specified.\n", - "Usage: gcloud config set SECTION/PROPERTY VALUE [optional flags]\n", - " optional flags may be --help | --installation\n", - "\n", - "For detailed information on this command and its flags, run:\n", - " gcloud config set --help\n" - ] - } + "data": { + "text/html": [ + "Query job 0ee1a08e-788e-4fc7-b061-52c23ab25d5a is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" + "text/plain": [ + "" ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "eF-Twtc4XGem" - }, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "from google.cloud import bigquery\n", - "from google.cloud import bigquery_connection_v1 as bq_connection" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "### Set BigQuery DataFrames options" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "NPPMuw2PXGeo" - }, - "outputs": [], - "source": [ - "# Note: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "\n", - "# Note: The location option is not required.\n", - "# It defaults to the location of the first table or query\n", - "# passed to read_gbq(). For APIs where a location can't be\n", - "# auto-detected, the location defaults to the \"US\" location.\n", - "bf.options.bigquery.location = REGION" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DTVtFlqeFbrU" - }, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6eytf4xQHzcF" - }, - "source": [ - "# Define the LLM model\n", - "\n", - "BigQuery DataFrames provides integration with [Gemini Models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) via Vertex AI.\n", - "\n", - "This section walks through a few steps required in order to use the model in your notebook." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "qUjT8nw-jIXp" - }, - "source": [ - "## Define the model\n", - "\n", - "Use `bigframes.ml.llm` to define the model:" - ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from bigframes.ml.llm import GeminiTextGenerator\n", + "\n", + "model = GeminiTextGenerator(model_name=\"gemini-2.0-flash-001\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GbW0oCnU1s1N" + }, + "source": [ + "# Read data from Cloud Storage into BigQuery DataFrames\n", + "\n", + "You can create a BigQuery DataFrames DataFrame by reading data from any of the following locations:\n", + "\n", + "* A local data file\n", + "* Data stored in a BigQuery table\n", + "* A data file stored in Cloud Storage\n", + "* An in-memory pandas DataFrame\n", + "\n", + "In this tutorial, you create BigQuery DataFrames DataFrames by reading two CSV files stored in Cloud Storage, one containing a list of DataFrame API names and one containing a list of Series API names." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "SchiTkQGIJog" + }, + "outputs": [], + "source": [ + "df_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/df.csv\")\n", + "series_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/series.csv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7OBjw2nmQY3-" + }, + "source": [ + "Take a peek at a few rows of data for each file:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "QCqgVCIsGGuv" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "sdjeXFwcHfl7" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 0ee1a08e-788e-4fc7-b061-52c23ab25d5a is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "data": { + "text/html": [ + "Query job 48be241c-ee93-4dfa-a9e3-66b64c4b5150 is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "from bigframes.ml.llm import GeminiTextGenerator\n", - "\n", - "model = GeminiTextGenerator(model_name=\"gemini-2.0-flash-001\")" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "GbW0oCnU1s1N" - }, - "source": [ - "# Read data from Cloud Storage into BigQuery DataFrames\n", - "\n", - "You can create a BigQuery DataFrames DataFrame by reading data from any of the following locations:\n", - "\n", - "* A local data file\n", - "* Data stored in a BigQuery table\n", - "* A data file stored in Cloud Storage\n", - "* An in-memory pandas DataFrame\n", - "\n", - "In this tutorial, you create BigQuery DataFrames DataFrames by reading two CSV files stored in Cloud Storage, one containing a list of DataFrame API names and one containing a list of Series API names." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "SchiTkQGIJog" - }, - "outputs": [], - "source": [ - "df_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/df.csv\")\n", - "series_api = bf.read_csv(\"gs://cloud-samples-data/vertex-ai/bigframe/series.csv\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "7OBjw2nmQY3-" - }, - "source": [ - "Take a peek at a few rows of data for each file:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "id": "QCqgVCIsGGuv" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 48be241c-ee93-4dfa-a9e3-66b64c4b5150 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 6af9caa5-4f7a-48f0-a7df-d692ee063b7e is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
API
0values
1dtypes
\n", - "

2 rows × 1 columns

\n", - "
[2 rows x 1 columns in total]" - ], - "text/plain": [ - " API\n", - "0 values\n", - "1 dtypes\n", - "\n", - "[2 rows x 1 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "Query job 6af9caa5-4f7a-48f0-a7df-d692ee063b7e is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "df_api.head(2)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "id": "BGJnZbgEGS5-" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 41e4f2e7-689a-45d9-bf92-4416f5560b81 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job aae0b164-f786-4734-8c79-2af9805af0cf is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
API
0shape
1size
\n", - "

2 rows × 1 columns

\n", - "
[2 rows x 1 columns in total]" - ], - "text/plain": [ - " API\n", - "0 shape\n", - "1 size\n", - "\n", - "[2 rows x 1 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
API
0values
1dtypes
\n", + "

2 rows × 1 columns

\n", + "
[2 rows x 1 columns in total]" ], - "source": [ - "series_api.head(2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "m3ZJEsi7SUKV" - }, - "source": [ - "# Generate code using the LLM model\n", - "\n", - "Prepare the prompts and send them to the LLM model for prediction." + "text/plain": [ + " API\n", + "0 values\n", + "1 dtypes\n", + "\n", + "[2 rows x 1 columns]" ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9EMAqR37AfLS" - }, - "source": [ - "## Prompt design in BigQuery DataFrames\n", - "\n", - "Designing prompts for LLMs is a fast growing area and you can read more in [this documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/introduction-prompt-design).\n", - "\n", - "For this tutorial, you use a simple prompt to ask the LLM model for sample code for each of the API methods (or rows) from the last step's DataFrames. The output is the new DataFrames `df_prompt` and `series_prompt`, which contain the full prompt text." - ] - }, + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_api.head(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "BGJnZbgEGS5-" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "id": "EDAaIwHpQCDZ" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 17f50c10-aa81-4023-b206-4ba59ddf2269 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job d6d217aa-a623-4ea4-83fb-8f1b8bfb8e68 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job a275a107-752e-46f8-be9f-9cb35eb6b0b9 is DONE. 132 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "0 Generate Pandas sample code for DataFrame.values\n", - "1 Generate Pandas sample code for DataFrame.dtypes\n", - "Name: API, dtype: string" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "Query job 41e4f2e7-689a-45d9-bf92-4416f5560b81 is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "df_prompt_prefix = \"Generate Pandas sample code for DataFrame.\"\n", - "series_prompt_prefix = \"Generate Pandas sample code for Series.\"\n", - "\n", - "df_prompt = (df_prompt_prefix + df_api['API'])\n", - "series_prompt = (series_prompt_prefix + series_api['API'])\n", - "\n", - "df_prompt.head(2)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "rwPLjqW2Ajzh" - }, - "source": [ - "## Make predictions using the LLM model\n", - "\n", - "Use the BigQuery DataFrames DataFrame containing the full prompt text as the input to the `predict` method. The `predict` method calls the LLM model and returns its generated text output back to two new BigQuery DataFrames DataFrames, `df_pred` and `series_pred`.\n", - "\n", - "Note: The predictions might take a few minutes to run." + "data": { + "text/html": [ + "Query job aae0b164-f786-4734-8c79-2af9805af0cf is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "id": "6i6HkFJZa8na" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 01f95d2d-901d-4edf-bd3a-245d17c31ef6 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 55927a6f-b023-479a-b9bf-826abde77111 is DONE. 584 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 445eb0af-f643-40c5-9c1e-25aa3db8374a is DONE. 146 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job ddee268c-773a-4dcc-b14c-ebdd90c2c347 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job d7f1eb26-28b2-44ba-8858-5cd4df8621bd is DONE. 904 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job f24d27a5-0e36-4fb5-953b-d09298f83af6 is DONE. 226 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
API
0shape
1size
\n", + "

2 rows × 1 columns

\n", + "
[2 rows x 1 columns in total]" ], - "source": [ - "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", - "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" + "text/plain": [ + " API\n", + "0 shape\n", + "1 size\n", + "\n", + "[2 rows x 1 columns]" ] - }, + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "series_api.head(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m3ZJEsi7SUKV" + }, + "source": [ + "# Generate code using the LLM model\n", + "\n", + "Prepare the prompts and send them to the LLM model for prediction." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Prompt design in BigQuery DataFrames\n", + "\n", + "Designing prompts for LLMs is a fast growing area and you can read more in [this documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/introduction-prompt-design).\n", + "\n", + "For this tutorial, you use a simple prompt to ask the LLM model for sample code for each of the API methods (or rows) from the last step's DataFrames. The output is the new DataFrames `df_prompt` and `series_prompt`, which contain the full prompt text." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "89cB8MW4UIdV" - }, - "source": [ - "Once the predictions are processed, take a look at the sample output from the LLM, which provides code samples for the API names listed in the DataFrames dataset." + "data": { + "text/html": [ + "Query job 17f50c10-aa81-4023-b206-4ba59ddf2269 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "id": "9A2gw6hP_2nX" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 65599c98-72ad-4088-8b09-f29bf05c164b is DONE. 21.8 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "```python\n", - "import pandas as pd\n", - "\n", - "# Create a DataFrame\n", - "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", - "\n", - "# Get the values as a NumPy array\n", - "values = df.values\n", - "\n", - "# Print the values\n", - "print(values)\n", - "```\n" - ] - } + "data": { + "text/html": [ + "Query job d6d217aa-a623-4ea4-83fb-8f1b8bfb8e68 is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "print(df_pred['ml_generate_text_llm_result'].iloc[0])" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "Fx4lsNqMorJ-" - }, - "source": [ - "# Manipulate LLM output using a remote function\n", - "\n", - "The output that the LLM provides often contains additional text beyond the code sample itself. Using BigQuery DataFrames, you can deploy custom Python functions that process and transform this output.\n", - "\n" + "data": { + "text/html": [ + "Query job a275a107-752e-46f8-be9f-9cb35eb6b0b9 is DONE. 132 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "d8L7SN03VByG" - }, - "source": [ - "Running the cell below creates a custom function that you can use to process the LLM output data in two ways:\n", - "1. Strip the LLM text output to include only the code block.\n", - "2. Substitute `import pandas as pd` with `import bigframes.pandas as bf` so that the resulting code block works with BigQuery DataFrames." + "data": { + "text/plain": [ + "0 Generate Pandas sample code for DataFrame.values\n", + "1 Generate Pandas sample code for DataFrame.dtypes\n", + "Name: API, dtype: string" ] - }, + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_prompt_prefix = \"Generate Pandas sample code for DataFrame.\"\n", + "series_prompt_prefix = \"Generate Pandas sample code for Series.\"\n", + "\n", + "df_prompt = (df_prompt_prefix + df_api['API'])\n", + "series_prompt = (series_prompt_prefix + series_api['API'])\n", + "\n", + "df_prompt.head(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Make predictions using the LLM model\n", + "\n", + "Use the BigQuery DataFrames DataFrame containing the full prompt text as the input to the `predict` method. The `predict` method calls the LLM model and returns its generated text output back to two new BigQuery DataFrames DataFrames, `df_pred` and `series_pred`.\n", + "\n", + "Note: The predictions might take a few minutes to run." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "id": "GskyyUQPowBT" - }, - "outputs": [], - "source": [ - "@bf.remote_function(cloud_function_service_account=\"default\")\n", - "def extract_code(text: str) -> str:\n", - " try:\n", - " res = text[text.find('\\n')+1:text.find('```', 3)]\n", - " res = res.replace(\"import pandas as pd\", \"import bigframes.pandas as bf\")\n", - " if \"import bigframes.pandas as bf\" not in res:\n", - " res = \"import bigframes.pandas as bf\\n\" + res\n", - " return res\n", - " except:\n", - " return \"\"" + "data": { + "text/html": [ + "Query job 01f95d2d-901d-4edf-bd3a-245d17c31ef6 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "hVQAoqBUOJQf" - }, - "source": [ - "The custom function is deployed as a Cloud Function, and then integrated with BigQuery as a [remote function](https://cloud.google.com/bigquery/docs/remote-functions). Save both of the function names so that you can clean them up at the end of this notebook." + "data": { + "text/html": [ + "Query job 55927a6f-b023-479a-b9bf-826abde77111 is DONE. 584 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "id": "PBlp-C-DOHRO" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cloud Function Name projects/swast-scratch/locations/us-central1/functions/bigframes-6e7606963c3f06b8181b3cb9449a4363\n", - "Remote Function Name swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bigframes_6e7606963c3f06b8181b3cb9449a4363\n" - ] - } + "data": { + "text/html": [ + "Query job 445eb0af-f643-40c5-9c1e-25aa3db8374a is DONE. 146 Bytes processed. Open Job" ], - "source": [ - "CLOUD_FUNCTION_NAME = format(extract_code.bigframes_cloud_function)\n", - "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", - "REMOTE_FUNCTION_NAME = format(extract_code.bigframes_remote_function)\n", - "print(\"Remote Function Name \" + REMOTE_FUNCTION_NAME)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "4FEucaiqVs3H" - }, - "source": [ - "Apply the custom function to each LLM output DataFrame to get the processed results:" + "data": { + "text/html": [ + "Query job ddee268c-773a-4dcc-b14c-ebdd90c2c347 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "id": "bsQ9cmoWo0Ps" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 047903f8-ea67-430a-8281-8fb5a119b779 is DONE. 21.8 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 793df956-0b1a-46ba-bb5e-e428171f3bd0 is DONE. 26.3 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "data": { + "text/html": [ + "Query job d7f1eb26-28b2-44ba-8858-5cd4df8621bd is DONE. 904 Bytes processed. Open Job" ], - "source": [ - "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", - "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "ujQVVuhfWA3y" - }, - "source": [ - "You can see the differences by inspecting the first row of data:" + "data": { + "text/html": [ + "Query job f24d27a5-0e36-4fb5-953b-d09298f83af6 is DONE. 226 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df_pred = model.predict(df_prompt.to_frame(), max_output_tokens=1024)\n", + "series_pred = model.predict(series_prompt.to_frame(), max_output_tokens=1024)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "89cB8MW4UIdV" + }, + "source": [ + "Once the predictions are processed, take a look at the sample output from the LLM, which provides code samples for the API names listed in the DataFrames dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "9A2gw6hP_2nX" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "id": "7yWzjhGy_zcy" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 6974c2b7-2ed9-4564-a80b-57aef6959e19 is DONE. 22.8 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "import bigframes.pandas as bf\n", - "\n", - "# Create a DataFrame\n", - "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", - "\n", - "# Get the values as a NumPy array\n", - "values = df.values\n", - "\n", - "# Print the values\n", - "print(values)\n", - "\n" - ] - } + "data": { + "text/html": [ + "Query job 65599c98-72ad-4088-8b09-f29bf05c164b is DONE. 21.8 kB processed. Open Job" ], - "source": [ - "print(df_code['code'].iloc[0])" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "GTRdUw-Ro5R1" - }, - "source": [ - "# Save the results to Cloud Storage\n", - "\n", - "BigQuery DataFrames lets you save a BigQuery DataFrames DataFrame as a CSV file in Cloud Storage for further use. Try that now with your processed LLM output data." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "```python\n", + "import pandas as pd\n", + "\n", + "# Create a DataFrame\n", + "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", + "\n", + "# Get the values as a NumPy array\n", + "values = df.values\n", + "\n", + "# Print the values\n", + "print(values)\n", + "```\n" + ] + } + ], + "source": [ + "print(df_pred['ml_generate_text_llm_result'].iloc[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, + "source": [ + "# Manipulate LLM output using a remote function\n", + "\n", + "The output that the LLM provides often contains additional text beyond the code sample itself. Using BigQuery DataFrames, you can deploy custom Python functions that process and transform this output.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d8L7SN03VByG" + }, + "source": [ + "Running the cell below creates a custom function that you can use to process the LLM output data in two ways:\n", + "1. Strip the LLM text output to include only the code block.\n", + "2. Substitute `import pandas as pd` with `import bigframes.pandas as bf` so that the resulting code block works with BigQuery DataFrames." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [], + "source": [ + "@bf.remote_function(cloud_function_service_account=\"default\")\n", + "def extract_code(text: str) -> str:\n", + " try:\n", + " res = text[text.find('\\n')+1:text.find('```', 3)]\n", + " res = res.replace(\"import pandas as pd\", \"import bigframes.pandas as bf\")\n", + " if \"import bigframes.pandas as bf\" not in res:\n", + " res = \"import bigframes.pandas as bf\\n\" + res\n", + " return res\n", + " except:\n", + " return \"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hVQAoqBUOJQf" + }, + "source": [ + "The custom function is deployed as a Cloud Function, and then integrated with BigQuery as a [remote function](https://cloud.google.com/bigquery/docs/remote-functions). Save both of the function names so that you can clean them up at the end of this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "PBlp-C-DOHRO" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "9DQ7eiQxPTi3" - }, - "source": [ - "Create a new Cloud Storage bucket with a unique name:" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloud Function Name projects/swast-scratch/locations/us-central1/functions/bigframes-6e7606963c3f06b8181b3cb9449a4363\n", + "Remote Function Name swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.bigframes_6e7606963c3f06b8181b3cb9449a4363\n" + ] + } + ], + "source": [ + "CLOUD_FUNCTION_NAME = format(extract_code.bigframes_cloud_function)\n", + "print(\"Cloud Function Name \" + CLOUD_FUNCTION_NAME)\n", + "REMOTE_FUNCTION_NAME = format(extract_code.bigframes_remote_function)\n", + "print(\"Remote Function Name \" + REMOTE_FUNCTION_NAME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4FEucaiqVs3H" + }, + "source": [ + "Apply the custom function to each LLM output DataFrame to get the processed results:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "id": "-J5LHgS6LLZ0" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating gs://code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/...\n" - ] - } + "data": { + "text/html": [ + "Query job 047903f8-ea67-430a-8281-8fb5a119b779 is DONE. 21.8 kB processed. Open Job" ], - "source": [ - "import uuid\n", - "BUCKET_ID = \"code-samples-\" + str(uuid.uuid1())\n", - "\n", - "!gcloud storage buckets create gs://{BUCKET_ID}" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "tyxZXj0UPYUv" - }, - "source": [ - "Use `to_csv` to write each BigQuery DataFrames DataFrame as a CSV file in the Cloud Storage bucket:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "id": "Zs_b5L-4IvER" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 81277037-032f-4557-a46e-1d39702f33d5 is DONE. 22.8 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 8dc5a38c-ac16-44e7-83dd-4187380f780f is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 9087a758-b1f9-4be7-889b-7761ef0ad966 is DONE. 27.7 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 6126ea72-c6f7-43f0-8888-e1c2a464a8a4 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "data": { + "text/html": [ + "Query job 793df956-0b1a-46ba-bb5e-e428171f3bd0 is DONE. 26.3 kB processed. Open Job" ], - "source": [ - "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", - "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "UDBtDlrTuuh8" - }, - "source": [ - "You can navigate to the Cloud Storage bucket browser to download the two files and view them.\n", - "\n", - "Run the following cell, and then follow the link to your Cloud Storage bucket browser:" + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df_code = df_pred.assign(code=df_pred['ml_generate_text_llm_result'].apply(extract_code))\n", + "series_code = series_pred.assign(code=series_pred['ml_generate_text_llm_result'].apply(extract_code))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ujQVVuhfWA3y" + }, + "source": [ + "You can see the differences by inspecting the first row of data:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "7yWzjhGy_zcy" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "id": "PspCXu-qu_ND" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://console.developers.google.com/storage/browser/code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/\n" - ] - } + "data": { + "text/html": [ + "Query job 6974c2b7-2ed9-4564-a80b-57aef6959e19 is DONE. 22.8 kB processed. Open Job" ], - "source": [ - "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "RGSvUk48RK20" - }, - "source": [ - "# Summary and next steps\n", - "\n", - "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", - "\n", - "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "import bigframes.pandas as bf\n", + "\n", + "# Create a DataFrame\n", + "df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", + "\n", + "# Get the values as a NumPy array\n", + "values = df.values\n", + "\n", + "# Print the values\n", + "print(values)\n", + "\n" + ] + } + ], + "source": [ + "print(df_code['code'].iloc[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, + "source": [ + "# Save the results to Cloud Storage\n", + "\n", + "BigQuery DataFrames lets you save a BigQuery DataFrames DataFrame as a CSV file in Cloud Storage for further use. Try that now with your processed LLM output data." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9DQ7eiQxPTi3" + }, + "source": [ + "Create a new Cloud Storage bucket with a unique name:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "-J5LHgS6LLZ0" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating gs://code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/...\n" + ] + } + ], + "source": [ + "import uuid\n", + "BUCKET_ID = \"code-samples-\" + str(uuid.uuid1())\n", + "\n", + "!gcloud storage buckets create gs://{BUCKET_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tyxZXj0UPYUv" + }, + "source": [ + "Use `to_csv` to write each BigQuery DataFrames DataFrame as a CSV file in the Cloud Storage bucket:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "Zs_b5L-4IvER" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bf.close_session()" + "data": { + "text/html": [ + "Query job 81277037-032f-4557-a46e-1d39702f33d5 is DONE. 22.8 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "id": "yw7A461XLjvW" - }, - "outputs": [], - "source": [ - "# # Delete the BigQuery Connection\n", - "# from google.cloud import bigquery_connection_v1 as bq_connection\n", - "# client = bq_connection.ConnectionServiceClient()\n", - "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", - "# client.delete_connection(name=CONNECTION_ID)\n", - "# print(f\"Deleted connection '{CONNECTION_ID}'.\")" + "data": { + "text/html": [ + "Query job 8dc5a38c-ac16-44e7-83dd-4187380f780f is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 25, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "# # Delete the Cloud Function\n", - "# ! gcloud functions delete {CLOUD_FUNCTION_NAME} --quiet\n", - "# # Delete the Remote Function\n", - "# REMOTE_FUNCTION_NAME = REMOTE_FUNCTION_NAME.replace(PROJECT_ID + \".\", \"\")\n", - "# ! bq rm --routine --force=true {REMOTE_FUNCTION_NAME}" + "data": { + "text/html": [ + "Query job 9087a758-b1f9-4be7-889b-7761ef0ad966 is DONE. 27.7 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "id": "iQFo6OUBLmi3" - }, - "outputs": [], - "source": [ - "# # Delete the Google Cloud Storage bucket and files\n", - "# ! gcloud storage rm gs://{BUCKET_ID} --recursive\n", - "# print(f\"Deleted bucket '{BUCKET_ID}'.\")" + "data": { + "text/html": [ + "Query job 6126ea72-c6f7-43f0-8888-e1c2a464a8a4 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" } - ], - "metadata": { - "colab": { - "provenance": [], - "toc_visible": true - }, - "kernelspec": { - "display_name": "venv (3.10.14)", - "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.10.14" + ], + "source": [ + "df_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/df_code*.csv\")\n", + "series_code[[\"code\"]].to_csv(f\"gs://{BUCKET_ID}/series_code*.csv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UDBtDlrTuuh8" + }, + "source": [ + "You can navigate to the Cloud Storage bucket browser to download the two files and view them.\n", + "\n", + "Run the following cell, and then follow the link to your Cloud Storage bucket browser:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "PspCXu-qu_ND" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://console.developers.google.com/storage/browser/code-samples-773ee0f2-e302-11ee-8298-4201c0a8181f/\n" + ] } + ], + "source": [ + "print(f'https://console.developers.google.com/storage/browser/{BUCKET_ID}/')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RGSvUk48RK20" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "You've used BigQuery DataFrames' integration with LLM models (`bigframes.ml.llm`) to generate code samples, and have tranformed LLM output by creating and using a custom function in BigQuery DataFrames.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bf.close_session()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "yw7A461XLjvW" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery Connection\n", + "# from google.cloud import bigquery_connection_v1 as bq_connection\n", + "# client = bq_connection.ConnectionServiceClient()\n", + "# CONNECTION_ID = f\"projects/{PROJECT_ID}/locations/{REGION}/connections/{CONN_NAME}\"\n", + "# client.delete_connection(name=CONNECTION_ID)\n", + "# print(f\"Deleted connection '{CONNECTION_ID}'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the Cloud Function\n", + "# ! gcloud functions delete {CLOUD_FUNCTION_NAME} --quiet\n", + "# # Delete the Remote Function\n", + "# REMOTE_FUNCTION_NAME = REMOTE_FUNCTION_NAME.replace(PROJECT_ID + \".\", \"\")\n", + "# ! bq rm --routine --force=true {REMOTE_FUNCTION_NAME}" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "id": "iQFo6OUBLmi3" + }, + "outputs": [], + "source": [ + "# # Delete the Google Cloud Storage bucket and files\n", + "# ! gcloud storage rm gs://{BUCKET_ID} --recursive\n", + "# print(f\"Deleted bucket '{BUCKET_ID}'.\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "venv (3.10.14)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "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.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 08891d2b445..42dd5a99ac0 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -26,7 +26,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Use BigQuery DataFrames to cluster and characterize complaints\n", + "# Use BigQuery DataFrames to cluster and characterize complaints", "\n", "\n", "\n", diff --git a/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb b/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb index 72651f19729..548162b2c6b 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb @@ -1,1790 +1,1790 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "id": "TpJu6BBeooES" - }, - "outputs": [], - "source": [ - "# Copyright 2023 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": { - "id": "EQbZKS7_ooET" - }, - "source": [ - "## Build a Vector Search application using BigQuery DataFrames (aka BigFrames)\n", - "\n", - "
\n", - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "vFMjpPBo9aVv" - }, - "source": [ - "**Author:** Sudipto Guha (Google)\n", - "\n", - "**Last updated:** March 16th 2025" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "TpJu6BBeooES" + }, + "outputs": [], + "source": [ + "# Copyright 2023 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": { + "id": "EQbZKS7_ooET" + }, + "source": [ + "# Build a Vector Search application using BigQuery DataFrames (aka BigFrames)", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vFMjpPBo9aVv" + }, + "source": [ + "**Author:** Sudipto Guha (Google)\n", + "\n", + "**Last updated:** March 16th 2025" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SHQ3Gx-oooEU" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook will guide you through a practical example of using [BigFrames](https://github.com/googleapis/python-bigquery-dataframes/issues) to perform [vector search](https://cloud.google.com/bigquery/docs/vector-search-intro) and analysis on a patent dataset within BigQuery. We will leverage Python and BigFrames to efficiently process, analyze, and gain insights from a large-scale dataset without moving data from BigQuery.\n", + "\n", + "Here's a breakdown of what we'll cover:\n", + "\n", + "1. **Data Ingestion and Embedding Generation:**\n", + "We will start by reading a public patent dataset directly from BigQuery into a BigFrames DataFrame.\n", + "We'll demonstrate how to use BigFrames' `TextEmbeddingGenerator` to create text embeddings for the patent abstracts. This process converts the textual data into numerical vectors that capture the semantic meaning of each abstract.\n", + "We'll show how BigFrames efficiently performs this embedding generation within BigQuery, avoiding data transfer to the client-side.\n", + "Finally, we'll store the generated embeddings back into a new BigQuery table for subsequent analysis.\n", + "\n", + "2. **Indexing and Similarity Search:**\n", + "Here we'll create a vector index using BigFrames to enable fast and scalable similarity searches.\n", + "We'll demonstrate how to create an IVF index for efficient approximate nearest neighbor searches.\n", + "We'll then perform a vector search using a sample query string to find patents that are semantically similar to the query. This showcases how vector search goes beyond keyword matching to find relevant results based on meaning.\n", + "\n", + "3. **AI-Powered Summarization with Retrieval Augmented Generation (RAG):**\n", + "To further enhance the analysis, we'll implement a RAG pipeline.\n", + "We'll retrieve the top most similar patents based on the vector search results from step 2.\n", + "We'll use BigFrames' `GeminiTextGenerator` to create a prompt for an LLM to generate a concise summary of the retrieved patents.\n", + "This demonstrates how to combine vector search with generative AI to extract and synthesize meaningful insights from complex patent data.\n", + "\n", + "\n", + "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EHjmqb-0ooEU" + }, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [BQ Patents Public Dataset](https://bigquery.cloud.google.com/dataset/patents-public-data:patentsview)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AqdihIDJooEU" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "* Generative AI support on Vertex AI\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models), [Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GqLjnm1hsKGU" + }, + "source": [ + "## Setup & initialization\n", + "\n", + "Make sure you have the required roles and permissions listed below:\n", + "\n", + "For [Vector embedding generation](https://cloud.google.com/bigquery/docs/generate-text-embedding#required_roles)\n", + "\n", + "For [Vector Index creation](https://cloud.google.com/bigquery/docs/vector-index#roles_and_permissions)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Z-mvYJUCooEV" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xn-v3mSvooEV" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com) to enable the following APIs:\n", + "\n", + " * BigQuery API\n", + " * BigQuery Connection API\n", + " * Vertex AI API\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ioydzb_8ooEV" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, see the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "executionInfo": { + "elapsed": 2, + "status": "ok", + "timestamp": 1742191597773, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "SHQ3Gx-oooEU" - }, - "source": [ - "## Overview\n", - "\n", - "This notebook will guide you through a practical example of using [BigFrames](https://github.com/googleapis/python-bigquery-dataframes/issues) to perform [vector search](https://cloud.google.com/bigquery/docs/vector-search-intro) and analysis on a patent dataset within BigQuery. We will leverage Python and BigFrames to efficiently process, analyze, and gain insights from a large-scale dataset without moving data from BigQuery.\n", - "\n", - "Here's a breakdown of what we'll cover:\n", - "\n", - "1. **Data Ingestion and Embedding Generation:**\n", - "We will start by reading a public patent dataset directly from BigQuery into a BigFrames DataFrame.\n", - "We'll demonstrate how to use BigFrames' `TextEmbeddingGenerator` to create text embeddings for the patent abstracts. This process converts the textual data into numerical vectors that capture the semantic meaning of each abstract.\n", - "We'll show how BigFrames efficiently performs this embedding generation within BigQuery, avoiding data transfer to the client-side.\n", - "Finally, we'll store the generated embeddings back into a new BigQuery table for subsequent analysis.\n", - "\n", - "2. **Indexing and Similarity Search:**\n", - "Here we'll create a vector index using BigFrames to enable fast and scalable similarity searches.\n", - "We'll demonstrate how to create an IVF index for efficient approximate nearest neighbor searches.\n", - "We'll then perform a vector search using a sample query string to find patents that are semantically similar to the query. This showcases how vector search goes beyond keyword matching to find relevant results based on meaning.\n", - "\n", - "3. **AI-Powered Summarization with Retrieval Augmented Generation (RAG):**\n", - "To further enhance the analysis, we'll implement a RAG pipeline.\n", - "We'll retrieve the top most similar patents based on the vector search results from step 2.\n", - "We'll use BigFrames' `GeminiTextGenerator` to create a prompt for an LLM to generate a concise summary of the retrieved patents.\n", - "This demonstrates how to combine vector search with generative AI to extract and synthesize meaningful insights from complex patent data.\n", - "\n", - "\n", - "We will tie these pieces together in Python using BigQuery DataFrames. [Click here](https://cloud.google.com/bigquery/docs/dataframes-quickstart) to learn more about BigQuery DataFrames!" - ] + "id": "b8bKCfIiooEV" + }, + "outputs": [], + "source": [ + "# set your project ID below\n", + "PROJECT_ID = \"bigframes-dev\" # @param {type:\"string\"}\n", + "\n", + "# set your region\n", + "REGION = \"US\" # @param {type: \"string\"}\n", + "\n", + "# Set the project id in gcloud\n", + "#! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GbUgWr6LooEV" + }, + "source": [ + "#### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U7ChP8jUooEV" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VfHOYcZZooEW" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "3cGhUVM0ooEW" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AoHnXlg-ooEW" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "cell_type": "markdown", - "metadata": { - "id": "EHjmqb-0ooEU" - }, - "source": [ - "### Dataset\n", - "\n", - "This notebook uses the [BQ Patents Public Dataset](https://bigquery.cloud.google.com/dataset/patents-public-data:patentsview)." - ] + "executionInfo": { + "elapsed": 2, + "status": "ok", + "timestamp": 1742191608487, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "AqdihIDJooEU" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "* BigQuery ML\n", - "* Generative AI support on Vertex AI\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models), [Generative AI support on Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models),\n", - "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] + "id": "j3lmnsh7ooEW", + "outputId": "eb68daf5-5558-487a-91d2-4b4f9e476da0" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a9gsyttuooEW" + }, + "source": [ + "Now we are ready to use BigQuery DataFrames!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xckgWno6ouHY" + }, + "source": [ + "## Step 1: Data Ingestion and Embedding Generation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hjg9jDN-ooEW" + }, + "source": [ + "Install libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "executionInfo": { + "elapsed": 947, + "status": "ok", + "timestamp": 1742195413800, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "GqLjnm1hsKGU" - }, - "source": [ - "## Setup & initialization\n", - "\n", - "Make sure you have the required roles and permissions listed below:\n", - "\n", - "For [Vector embedding generation](https://cloud.google.com/bigquery/docs/generate-text-embedding#required_roles)\n", - "\n", - "For [Vector Index creation](https://cloud.google.com/bigquery/docs/vector-index#roles_and_permissions)" - ] + "id": "R7STCS8xB5d2" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bf\n", + "import bigframes.ml as bf_ml\n", + "import bigframes.bigquery as bf_bq\n", + "import bigframes.ml.llm as bf_llm\n", + "\n", + "\n", + "from google.cloud import bigquery\n", + "from google.cloud import storage\n", + "\n", + "# Construct a BigQuery client object.\n", + "client = bigquery.Client()\n", + "\n", + "import pandas as pd\n", + "from IPython.display import Image, display\n", + "from PIL import Image as PILImage\n", + "import io\n", + "\n", + "import json\n", + "from IPython.display import Markdown\n", + "\n", + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bf.options.bigquery.project = PROJECT_ID\n", + "bf.options.bigquery.location = REGION\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iOFF9hrvs5WE" + }, + "source": [ + "Partial ordering mode allows BigQuery DataFrames to push down many more row and column filters. On large clustered and partitioned tables, this can greatly reduce the number of bytes scanned and computation slots used. This [blog post](https://medium.com/google-cloud/introducing-partial-ordering-mode-for-bigquery-dataframes-bigframes-ec35841d95c0) goes over it in more detail." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "executionInfo": { + "elapsed": 2, + "status": "ok", + "timestamp": 1742191620533, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "Z-mvYJUCooEV" - }, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] + "id": "9Gil1Oaas7KA" + }, + "outputs": [], + "source": [ + "bf.options.bigquery.ordering_mode = \"partial\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XGaGyyZsooEW" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v6FGschEowht" + }, + "source": [ + "Data Input - read the data from a publicly available BigQuery dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "cell_type": "markdown", - "metadata": { - "id": "xn-v3mSvooEV" - }, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Click here](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com) to enable the following APIs:\n", - "\n", - " * BigQuery API\n", - " * BigQuery Connection API\n", - " * Vertex AI API\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] + "executionInfo": { + "elapsed": 468, + "status": "ok", + "timestamp": 1742192516923, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "Ioydzb_8ooEV" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, see the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] + "id": "zDSwoBo1CU3G", + "outputId": "83edbc2f-5a23-407b-8890-f968eb31be44" + }, + "outputs": [], + "source": [ + "publications = bf.read_gbq('patents-public-data.google_patents_research.publications')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "executionInfo": { - "elapsed": 2, - "status": "ok", - "timestamp": 1742191597773, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "b8bKCfIiooEV" - }, - "outputs": [], - "source": [ - "# set your project ID below\n", - "PROJECT_ID = \"bigframes-dev\" # @param {type:\"string\"}\n", - "\n", - "# set your region\n", - "REGION = \"US\" # @param {type: \"string\"}\n", - "\n", - "# Set the project id in gcloud\n", - "#! gcloud config set project {PROJECT_ID}" - ] + "executionInfo": { + "elapsed": 6697, + "status": "ok", + "timestamp": 1742192524632, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "GbUgWr6LooEV" - }, - "source": [ - "#### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." - ] + "id": "tYDoaKgJChiq", + "outputId": "9174da29-a051-4a99-e38f-6a2b09cfe4e9" + }, + "outputs": [], + "source": [ + "## create patents base table (subset of 10k out of ~110M records)\n", + "\n", + "keep = (publications.embedding_v1.str.len() > 0) & (publications.title.str.len() > 0) & (publications.abstract.str.len() > 30)\n", + "\n", + "## Choose 10000 random rows to analyze\n", + "publications = publications[keep].peek(10000)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 556 }, - { - "cell_type": "markdown", - "metadata": { - "id": "U7ChP8jUooEV" - }, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." - ] + "executionInfo": { + "elapsed": 6, + "status": "ok", + "timestamp": 1742191801044, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "XmqdJInztzPl", + "outputId": "ae05f3a6-edeb-423a-c061-c416717e1ec5" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "VfHOYcZZooEW" - }, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" + "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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
publication_numbertitletitle_translatedabstractabstract_translatedcpccpc_lowcpc_inventive_lowtop_termssimilarurlcountrypublication_descriptioncited_byembedding_v1
0WO-2007022924-B1Pharmaceutical compositions with melting point...FalseThe invention relates to the use of chemical f...False[{'code': 'A61K47/32', 'inventive': True, 'fir...['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A...['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A...['composition' 'mucosa' 'melting point' 'agent...[{'publication_number': 'WO-2007022924-B1', 'a...https://patents.google.com/patent/WO2007022924B1WIPO (PCT)Amended claims[][ 5.3550040e-02 -9.3632710e-02 1.4337189e-02 ...
1WO-03043855-B1Convenience lighting for interior and exterior...FalseA lighting apparatus for a vehicle(21) include...False[{'code': 'B60Q1/247', 'inventive': True, 'fir...['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ...['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ...['vehicle' 'light' 'apparatus defined' 'pillar...[{'publication_number': 'WO-03043855-B1', 'app...https://patents.google.com/patent/WO2003043855B1WIPO (PCT)Amended claims[][ 0.00484032 -0.02695554 -0.20798226 -0.207528...
2AU-2020396918-A2Shot detection and verification systemFalseA shot detection system for a projectile weapo...False[{'code': 'F41A19/01', 'inventive': True, 'fir...['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04...['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04...['interest' 'region' 'property' 'shot' 'test' ...[{'publication_number': 'US-2023228510-A1', 'a...https://patents.google.com/patent/AU2020396918A2AustraliaAmended post open to public inspection[][-1.49729420e-02 -2.27105440e-01 -2.68012730e-...
3PL-347539-A1Concrete mix of increased fire resistanceFalseThe burning resistance of concrete containing ...False[{'code': 'Y02W30/91', 'inventive': False, 'fi...['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y...['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y...['fire resistance' 'concrete mix' 'increased f...[{'publication_number': 'DK-1564194-T3', 'appl...https://patents.google.com/patent/PL347539A1PolandApplication[][ 0.01849568 -0.05340371 -0.19257502 -0.174919...
4AU-PS049302-A0Methods and systems (ap53)FalseA charging stand for charging a mobile phone, ...False[{'code': 'H02J7/00', 'inventive': True, 'firs...['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1...['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1...['connection pin' 'mobile phone' 'cartridge' '...[{'publication_number': 'AU-PS049302-A0', 'app...https://patents.google.com/patent/AUPS049302A0AustraliaApplication filed, as announced in the Gazette...[][ 0.00064732 -0.2136009 0.0040593 -0.024562...
\n", + "
" + ], + "text/plain": [ + " publication_number title \\\n", + "0 WO-2007022924-B1 Pharmaceutical compositions with melting point... \n", + "1 WO-03043855-B1 Convenience lighting for interior and exterior... \n", + "2 AU-2020396918-A2 Shot detection and verification system \n", + "3 PL-347539-A1 Concrete mix of increased fire resistance \n", + "4 AU-PS049302-A0 Methods and systems (ap53) \n", + "\n", + " title_translated abstract \\\n", + "0 False The invention relates to the use of chemical f... \n", + "1 False A lighting apparatus for a vehicle(21) include... \n", + "2 False A shot detection system for a projectile weapo... \n", + "3 False The burning resistance of concrete containing ... \n", + "4 False A charging stand for charging a mobile phone, ... \n", + "\n", + " abstract_translated cpc \\\n", + "0 False [{'code': 'A61K47/32', 'inventive': True, 'fir... \n", + "1 False [{'code': 'B60Q1/247', 'inventive': True, 'fir... \n", + "2 False [{'code': 'F41A19/01', 'inventive': True, 'fir... \n", + "3 False [{'code': 'Y02W30/91', 'inventive': False, 'fi... \n", + "4 False [{'code': 'H02J7/00', 'inventive': True, 'firs... \n", + "\n", + " cpc_low \\\n", + "0 ['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A... \n", + "1 ['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ... \n", + "2 ['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04... \n", + "3 ['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y... \n", + "4 ['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1... \n", + "\n", + " cpc_inventive_low \\\n", + "0 ['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A... \n", + "1 ['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ... \n", + "2 ['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04... \n", + "3 ['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y... \n", + "4 ['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1... \n", + "\n", + " top_terms \\\n", + "0 ['composition' 'mucosa' 'melting point' 'agent... \n", + "1 ['vehicle' 'light' 'apparatus defined' 'pillar... \n", + "2 ['interest' 'region' 'property' 'shot' 'test' ... \n", + "3 ['fire resistance' 'concrete mix' 'increased f... \n", + "4 ['connection pin' 'mobile phone' 'cartridge' '... \n", + "\n", + " similar \\\n", + "0 [{'publication_number': 'WO-2007022924-B1', 'a... \n", + "1 [{'publication_number': 'WO-03043855-B1', 'app... \n", + "2 [{'publication_number': 'US-2023228510-A1', 'a... \n", + "3 [{'publication_number': 'DK-1564194-T3', 'appl... \n", + "4 [{'publication_number': 'AU-PS049302-A0', 'app... \n", + "\n", + " url country \\\n", + "0 https://patents.google.com/patent/WO2007022924B1 WIPO (PCT) \n", + "1 https://patents.google.com/patent/WO2003043855B1 WIPO (PCT) \n", + "2 https://patents.google.com/patent/AU2020396918A2 Australia \n", + "3 https://patents.google.com/patent/PL347539A1 Poland \n", + "4 https://patents.google.com/patent/AUPS049302A0 Australia \n", + "\n", + " publication_description cited_by \\\n", + "0 Amended claims [] \n", + "1 Amended claims [] \n", + "2 Amended post open to public inspection [] \n", + "3 Application [] \n", + "4 Application filed, as announced in the Gazette... [] \n", + "\n", + " embedding_v1 \n", + "0 [ 5.3550040e-02 -9.3632710e-02 1.4337189e-02 ... \n", + "1 [ 0.00484032 -0.02695554 -0.20798226 -0.207528... \n", + "2 [-1.49729420e-02 -2.27105440e-01 -2.68012730e-... \n", + "3 [ 0.01849568 -0.05340371 -0.19257502 -0.174919... \n", + "4 [ 0.00064732 -0.2136009 0.0040593 -0.024562... " ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## take a look at the sample dataset\n", + "\n", + "publications.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wl2o-NYMoygb" + }, + "source": [ + "Generate the text embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "3cGhUVM0ooEW" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] + "executionInfo": { + "elapsed": 4528, + "status": "ok", + "timestamp": 1742192047236, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "li38q8FzDDMu", + "outputId": "b8c1bd38-b484-4f71-bd38-927c8677d0c5" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "AoHnXlg-ooEW" - }, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" + "data": { + "text/html": [ + "Query job 0e9d9117-4981-4f5c-b785-ed831c08e7aa is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "executionInfo": { - "elapsed": 2, - "status": "ok", - "timestamp": 1742191608487, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "j3lmnsh7ooEW", - "outputId": "eb68daf5-5558-487a-91d2-4b4f9e476da0" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" + "data": { + "text/html": [ + "Query job fa4f1a54-85d4-4030-992e-fddda5edf3e3 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from bigframes.ml.llm import TextEmbeddingGenerator\n", + "\n", + "text_model = TextEmbeddingGenerator(\n", + " model_name=\"text-embedding-005\",\n", + " # No connection id needed\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 139 }, - { - "cell_type": "markdown", - "metadata": { - "id": "a9gsyttuooEW" - }, - "source": [ - "Now we are ready to use BigQuery DataFrames!" - ] + "executionInfo": { + "elapsed": 126632, + "status": "ok", + "timestamp": 1742192656608, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "b5HHZob_u61B", + "outputId": "c9ecc5fd-5d11-4fd8-f59b-9dce4e12e371" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "xckgWno6ouHY" - }, - "source": [ - "## Step 1: Data Ingestion and Embedding Generation" + "data": { + "text/html": [ + "Load job 70377d71-bb13-46af-80c1-71ef16bf2949 is DONE. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "Hjg9jDN-ooEW" - }, - "source": [ - "Install libraries" + "data": { + "text/html": [ + "Query job cc3b609d-b6b7-404f-9447-c76d3a52698b is DONE. 9.5 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "executionInfo": { - "elapsed": 947, - "status": "ok", - "timestamp": 1742195413800, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "R7STCS8xB5d2" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bf\n", - "import bigframes.ml as bf_ml\n", - "import bigframes.bigquery as bf_bq\n", - "import bigframes.ml.llm as bf_llm\n", - "\n", - "\n", - "from google.cloud import bigquery\n", - "from google.cloud import storage\n", - "\n", - "# Construct a BigQuery client object.\n", - "client = bigquery.Client()\n", - "\n", - "import pandas as pd\n", - "from IPython.display import Image, display\n", - "from PIL import Image as PILImage\n", - "import io\n", - "\n", - "import json\n", - "from IPython.display import Markdown\n", - "\n", - "# Note: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bf.options.bigquery.project = PROJECT_ID\n", - "bf.options.bigquery.location = REGION\n", - "\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" + ] + } + ], + "source": [ + "## rename abstract column to content as the desired column on which embedding will be generated\n", + "publications = publications[[\"publication_number\", \"title\", \"abstract\"]].rename(columns={'abstract': 'content'})\n", + "\n", + "## generate the embeddings\n", + "## takes ~2-3 mins to run\n", + "embedding = text_model.predict(publications)[[\"publication_number\", \"title\", \"content\", \"ml_generate_embedding_result\",\"ml_generate_embedding_status\"]]\n", + "\n", + "## filter out rows where the embedding generation failed. the embedding status value is empty if the embedding generation was successful\n", + "embedding = embedding[~embedding[\"ml_generate_embedding_status\"].isnull()]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 464 }, - { - "cell_type": "markdown", - "metadata": { - "id": "iOFF9hrvs5WE" - }, - "source": [ - "Partial ordering mode allows BigQuery DataFrames to push down many more row and column filters. On large clustered and partitioned tables, this can greatly reduce the number of bytes scanned and computation slots used. This [blog post](https://medium.com/google-cloud/introducing-partial-ordering-mode-for-bigquery-dataframes-bigframes-ec35841d95c0) goes over it in more detail." - ] + "executionInfo": { + "elapsed": 6715, + "status": "ok", + "timestamp": 1742192727525, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "OIT5FbqAwqG5", + "outputId": "d04c994a-a0c8-44b0-e897-d871036eeb1f" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "executionInfo": { - "elapsed": 2, - "status": "ok", - "timestamp": 1742191620533, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "9Gil1Oaas7KA" - }, - "outputs": [], - "source": [ - "bf.options.bigquery.ordering_mode = \"partial\"" + "data": { + "text/html": [ + "Query job 5b15fc4a-fa9a-4608-825f-be5af9953a38 is DONE. 71.0 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "XGaGyyZsooEW" - }, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bf.close_session()`. After that, you can reuse `bf.options.bigquery.location` to specify another location." + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
publication_numbertitlecontentml_generate_embedding_resultml_generate_embedding_status
5611WO-2014005277-A1Resource management in a cloud computing envir...Technologies and implementations for managing ...[-2.92946529e-02 -1.24640828e-02 1.27173709e-...
6895AU-2011325479-B27-([1,2,3]triazol-4-yl)-pyrrolo[2,3-b]pyrazine...Compounds of formula I, in which R[-6.45397678e-02 1.19616119e-02 -9.85191786e-...
6IL-45347-A7h-indolizino(5,6,7-ij)isoquinoline derivative...Compounds of the formula:\\n[US3946019A][-3.82784344e-02 -2.31682733e-02 -4.35006060e-...
5923WO-2005111625-A3Method to predict prostate cancerA method for predicting the probability or ris...[ 0.02480386 -0.01648765 0.03873815 -0.025998...
6370US-7868678-B2Configurable differential linesEmbodiments related to configurable differenti...[ 2.71715336e-02 -1.93733890e-02 2.82729534e-...
\n", + "

5 rows × 5 columns

\n", + "
[5 rows x 5 columns in total]" + ], + "text/plain": [ + " publication_number title \\\n", + "5611 WO-2014005277-A1 Resource management in a cloud computing envir... \n", + "6895 AU-2011325479-B2 7-([1,2,3]triazol-4-yl)-pyrrolo[2,3-b]pyrazine... \n", + "6 IL-45347-A 7h-indolizino(5,6,7-ij)isoquinoline derivative... \n", + "5923 WO-2005111625-A3 Method to predict prostate cancer \n", + "6370 US-7868678-B2 Configurable differential lines \n", + "\n", + " content \\\n", + "5611 Technologies and implementations for managing ... \n", + "6895 Compounds of formula I, in which R \n", + "6 Compounds of the formula:\\n[US3946019A] \n", + "5923 A method for predicting the probability or ris... \n", + "6370 Embodiments related to configurable differenti... \n", + "\n", + " ml_generate_embedding_result \\\n", + "5611 [-2.92946529e-02 -1.24640828e-02 1.27173709e-... \n", + "6895 [-6.45397678e-02 1.19616119e-02 -9.85191786e-... \n", + "6 [-3.82784344e-02 -2.31682733e-02 -4.35006060e-... \n", + "5923 [ 0.02480386 -0.01648765 0.03873815 -0.025998... \n", + "6370 [ 2.71715336e-02 -1.93733890e-02 2.82729534e-... \n", + "\n", + " ml_generate_embedding_status \n", + "5611 \n", + "6895 \n", + "6 \n", + "5923 \n", + "6370 \n", + "\n", + "[5 rows x 5 columns]" ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "embedding.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 53 }, - { - "cell_type": "markdown", - "metadata": { - "id": "v6FGschEowht" - }, - "source": [ - "Data Input - read the data from a publicly available BigQuery dataset" - ] + "executionInfo": { + "elapsed": 6590, + "status": "ok", + "timestamp": 1742192833667, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "GP3ZqX_bxLGq", + "outputId": "fb823ea2-e47c-415f-84d4-543dd3291e15" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "executionInfo": { - "elapsed": 468, - "status": "ok", - "timestamp": 1742192516923, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "zDSwoBo1CU3G", - "outputId": "83edbc2f-5a23-407b-8890-f968eb31be44" - }, - "outputs": [], - "source": [ - "publications = bf.read_gbq('patents-public-data.google_patents_research.publications')" + "data": { + "text/html": [ + "Query job 06ce090b-e3f9-4252-b847-45c2a296ca61 is DONE. 70.9 MB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "executionInfo": { - "elapsed": 6697, - "status": "ok", - "timestamp": 1742192524632, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "tYDoaKgJChiq", - "outputId": "9174da29-a051-4a99-e38f-6a2b09cfe4e9" - }, - "outputs": [], - "source": [ - "## create patents base table (subset of 10k out of ~110M records)\n", - "\n", - "keep = (publications.embedding_v1.str.len() > 0) & (publications.title.str.len() > 0) & (publications.abstract.str.len() > 30)\n", - "\n", - "## Choose 10000 random rows to analyze\n", - "publications = publications[keep].peek(10000)" + "data": { + "text/plain": [ + "'my_dataset.my_embeddings_table'" ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# store embeddings in a BQ table\n", + "DATASET_ID = \"my_dataset\" # @param {type:\"string\"}\n", + "TEXT_EMBEDDING_TABLE_ID = \"my_embeddings_table\" # @param {type:\"string\"}\n", + "embedding.to_gbq(f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\", if_exists='replace')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OUZ3NNbzo1Tb" + }, + "source": [ + "## Step 2: Indexing and Similarity Search" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvJH2FCmynMm" + }, + "source": [ + "### [Create a Vector Index](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_create_vector_index) using BigFrames\n", + "\n", + "\n", + "**Index Type**\n", + "\n", + "The algorithm to use to build the vector index.\n", + "The supported values are IVF and TREE_AH." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 556 - }, - "executionInfo": { - "elapsed": 6, - "status": "ok", - "timestamp": 1742191801044, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "XmqdJInztzPl", - "outputId": "ae05f3a6-edeb-423a-c061-c416717e1ec5" - }, - "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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
publication_numbertitletitle_translatedabstractabstract_translatedcpccpc_lowcpc_inventive_lowtop_termssimilarurlcountrypublication_descriptioncited_byembedding_v1
0WO-2007022924-B1Pharmaceutical compositions with melting point...FalseThe invention relates to the use of chemical f...False[{'code': 'A61K47/32', 'inventive': True, 'fir...['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A...['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A...['composition' 'mucosa' 'melting point' 'agent...[{'publication_number': 'WO-2007022924-B1', 'a...https://patents.google.com/patent/WO2007022924B1WIPO (PCT)Amended claims[][ 5.3550040e-02 -9.3632710e-02 1.4337189e-02 ...
1WO-03043855-B1Convenience lighting for interior and exterior...FalseA lighting apparatus for a vehicle(21) include...False[{'code': 'B60Q1/247', 'inventive': True, 'fir...['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ...['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ...['vehicle' 'light' 'apparatus defined' 'pillar...[{'publication_number': 'WO-03043855-B1', 'app...https://patents.google.com/patent/WO2003043855B1WIPO (PCT)Amended claims[][ 0.00484032 -0.02695554 -0.20798226 -0.207528...
2AU-2020396918-A2Shot detection and verification systemFalseA shot detection system for a projectile weapo...False[{'code': 'F41A19/01', 'inventive': True, 'fir...['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04...['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04...['interest' 'region' 'property' 'shot' 'test' ...[{'publication_number': 'US-2023228510-A1', 'a...https://patents.google.com/patent/AU2020396918A2AustraliaAmended post open to public inspection[][-1.49729420e-02 -2.27105440e-01 -2.68012730e-...
3PL-347539-A1Concrete mix of increased fire resistanceFalseThe burning resistance of concrete containing ...False[{'code': 'Y02W30/91', 'inventive': False, 'fi...['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y...['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y...['fire resistance' 'concrete mix' 'increased f...[{'publication_number': 'DK-1564194-T3', 'appl...https://patents.google.com/patent/PL347539A1PolandApplication[][ 0.01849568 -0.05340371 -0.19257502 -0.174919...
4AU-PS049302-A0Methods and systems (ap53)FalseA charging stand for charging a mobile phone, ...False[{'code': 'H02J7/00', 'inventive': True, 'firs...['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1...['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1...['connection pin' 'mobile phone' 'cartridge' '...[{'publication_number': 'AU-PS049302-A0', 'app...https://patents.google.com/patent/AUPS049302A0AustraliaApplication filed, as announced in the Gazette...[][ 0.00064732 -0.2136009 0.0040593 -0.024562...
\n", - "
" - ], - "text/plain": [ - " publication_number title \\\n", - "0 WO-2007022924-B1 Pharmaceutical compositions with melting point... \n", - "1 WO-03043855-B1 Convenience lighting for interior and exterior... \n", - "2 AU-2020396918-A2 Shot detection and verification system \n", - "3 PL-347539-A1 Concrete mix of increased fire resistance \n", - "4 AU-PS049302-A0 Methods and systems (ap53) \n", - "\n", - " title_translated abstract \\\n", - "0 False The invention relates to the use of chemical f... \n", - "1 False A lighting apparatus for a vehicle(21) include... \n", - "2 False A shot detection system for a projectile weapo... \n", - "3 False The burning resistance of concrete containing ... \n", - "4 False A charging stand for charging a mobile phone, ... \n", - "\n", - " abstract_translated cpc \\\n", - "0 False [{'code': 'A61K47/32', 'inventive': True, 'fir... \n", - "1 False [{'code': 'B60Q1/247', 'inventive': True, 'fir... \n", - "2 False [{'code': 'F41A19/01', 'inventive': True, 'fir... \n", - "3 False [{'code': 'Y02W30/91', 'inventive': False, 'fi... \n", - "4 False [{'code': 'H02J7/00', 'inventive': True, 'firs... \n", - "\n", - " cpc_low \\\n", - "0 ['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A... \n", - "1 ['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ... \n", - "2 ['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04... \n", - "3 ['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y... \n", - "4 ['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1... \n", - "\n", - " cpc_inventive_low \\\n", - "0 ['A61K47/32' 'A61K47/30' 'A61K47/00' 'A61K' 'A... \n", - "1 ['B60Q1/247' 'B60Q1/24' 'B60Q1/02' 'B60Q1/00' ... \n", - "2 ['F41A19/01' 'F41A19/00' 'F41A' 'F41' 'F' 'H04... \n", - "3 ['Y02W30/91' 'Y02W30/50' 'Y02W30/00' 'Y02W' 'Y... \n", - "4 ['H02J7/00' 'H02J' 'H02' 'H' 'H04B1/40' 'H04B1... \n", - "\n", - " top_terms \\\n", - "0 ['composition' 'mucosa' 'melting point' 'agent... \n", - "1 ['vehicle' 'light' 'apparatus defined' 'pillar... \n", - "2 ['interest' 'region' 'property' 'shot' 'test' ... \n", - "3 ['fire resistance' 'concrete mix' 'increased f... \n", - "4 ['connection pin' 'mobile phone' 'cartridge' '... \n", - "\n", - " similar \\\n", - "0 [{'publication_number': 'WO-2007022924-B1', 'a... \n", - "1 [{'publication_number': 'WO-03043855-B1', 'app... \n", - "2 [{'publication_number': 'US-2023228510-A1', 'a... \n", - "3 [{'publication_number': 'DK-1564194-T3', 'appl... \n", - "4 [{'publication_number': 'AU-PS049302-A0', 'app... \n", - "\n", - " url country \\\n", - "0 https://patents.google.com/patent/WO2007022924B1 WIPO (PCT) \n", - "1 https://patents.google.com/patent/WO2003043855B1 WIPO (PCT) \n", - "2 https://patents.google.com/patent/AU2020396918A2 Australia \n", - "3 https://patents.google.com/patent/PL347539A1 Poland \n", - "4 https://patents.google.com/patent/AUPS049302A0 Australia \n", - "\n", - " publication_description cited_by \\\n", - "0 Amended claims [] \n", - "1 Amended claims [] \n", - "2 Amended post open to public inspection [] \n", - "3 Application [] \n", - "4 Application filed, as announced in the Gazette... [] \n", - "\n", - " embedding_v1 \n", - "0 [ 5.3550040e-02 -9.3632710e-02 1.4337189e-02 ... \n", - "1 [ 0.00484032 -0.02695554 -0.20798226 -0.207528... \n", - "2 [-1.49729420e-02 -2.27105440e-01 -2.68012730e-... \n", - "3 [ 0.01849568 -0.05340371 -0.19257502 -0.174919... \n", - "4 [ 0.00064732 -0.2136009 0.0040593 -0.024562... " - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "## take a look at the sample dataset\n", - "\n", - "publications.head(5)" - ] + "executionInfo": { + "elapsed": 3882, + "status": "ok", + "timestamp": 1742193028877, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "Wl2o-NYMoygb" - }, - "source": [ - "Generate the text embeddings" - ] + "id": "6SBVdv6gyU5A", + "outputId": "6583e113-de27-4b44-972d-c1cc061e3c76" + }, + "outputs": [], + "source": [ + "## create vector index (note only works of tables >5000 rows)\n", + "\n", + "bf_bq.create_vector_index(\n", + " table_id = f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", + " column_name = \"ml_generate_embedding_result\",\n", + " replace= True,\n", + " index_name = \"bf_python_index\",\n", + " distance_type=\"cosine\",\n", + " index_type= \"ivf\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bo8mBbRLzCOA" + }, + "source": [ + "### Vector Search (semantic search) using Vector Index\n", + "\n", + "ANN (approx nearest neighbor) search using the created vector index" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "executionInfo": { + "elapsed": 639, + "status": "ok", + "timestamp": 1742194606771, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "executionInfo": { - "elapsed": 4528, - "status": "ok", - "timestamp": 1742192047236, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "li38q8FzDDMu", - "outputId": "b8c1bd38-b484-4f71-bd38-927c8677d0c5" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 0e9d9117-4981-4f5c-b785-ed831c08e7aa is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job fa4f1a54-85d4-4030-992e-fddda5edf3e3 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from bigframes.ml.llm import TextEmbeddingGenerator\n", - "\n", - "text_model = TextEmbeddingGenerator(\n", - " model_name=\"text-embedding-005\",\n", - " # No connection id needed\n", - ")" - ] + "id": "v19BJm_wzPdZ" + }, + "outputs": [], + "source": [ + "## Set variable for vector search\n", + "\n", + "TEXT_SEARCH_STRING = \"Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\" ## replace with whatever search string you want to use for the vector search\n", + "FRACTION_LISTS_TO_SEARCH = 0.01" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 121 }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 139 - }, - "executionInfo": { - "elapsed": 126632, - "status": "ok", - "timestamp": 1742192656608, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "b5HHZob_u61B", - "outputId": "c9ecc5fd-5d11-4fd8-f59b-9dce4e12e371" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Load job 70377d71-bb13-46af-80c1-71ef16bf2949 is DONE. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job cc3b609d-b6b7-404f-9447-c76d3a52698b is DONE. 9.5 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" - ] - } - ], - "source": [ - "## rename abstract column to content as the desired column on which embedding will be generated\n", - "publications = publications[[\"publication_number\", \"title\", \"abstract\"]].rename(columns={'abstract': 'content'})\n", - "\n", - "## generate the embeddings\n", - "## takes ~2-3 mins to run\n", - "embedding = text_model.predict(publications)[[\"publication_number\", \"title\", \"content\", \"ml_generate_embedding_result\",\"ml_generate_embedding_status\"]]\n", - "\n", - "## filter out rows where the embedding generation failed. the embedding status value is empty if the embedding generation was successful\n", - "embedding = embedding[~embedding[\"ml_generate_embedding_status\"].isnull()]\n" - ] + "executionInfo": { + "elapsed": 6927, + "status": "ok", + "timestamp": 1742194625774, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "pAQY1ejpzPap", + "outputId": "485698ad-ac6e-4c93-844e-5d0f30aff13a" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 464 - }, - "executionInfo": { - "elapsed": 6715, - "status": "ok", - "timestamp": 1742192727525, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "OIT5FbqAwqG5", - "outputId": "d04c994a-a0c8-44b0-e897-d871036eeb1f" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 5b15fc4a-fa9a-4608-825f-be5af9953a38 is DONE. 71.0 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
publication_numbertitlecontentml_generate_embedding_resultml_generate_embedding_status
5611WO-2014005277-A1Resource management in a cloud computing envir...Technologies and implementations for managing ...[-2.92946529e-02 -1.24640828e-02 1.27173709e-...
6895AU-2011325479-B27-([1,2,3]triazol-4-yl)-pyrrolo[2,3-b]pyrazine...Compounds of formula I, in which R[-6.45397678e-02 1.19616119e-02 -9.85191786e-...
6IL-45347-A7h-indolizino(5,6,7-ij)isoquinoline derivative...Compounds of the formula:\\n[US3946019A][-3.82784344e-02 -2.31682733e-02 -4.35006060e-...
5923WO-2005111625-A3Method to predict prostate cancerA method for predicting the probability or ris...[ 0.02480386 -0.01648765 0.03873815 -0.025998...
6370US-7868678-B2Configurable differential linesEmbodiments related to configurable differenti...[ 2.71715336e-02 -1.93733890e-02 2.82729534e-...
\n", - "

5 rows × 5 columns

\n", - "
[5 rows x 5 columns in total]" - ], - "text/plain": [ - " publication_number title \\\n", - "5611 WO-2014005277-A1 Resource management in a cloud computing envir... \n", - "6895 AU-2011325479-B2 7-([1,2,3]triazol-4-yl)-pyrrolo[2,3-b]pyrazine... \n", - "6 IL-45347-A 7h-indolizino(5,6,7-ij)isoquinoline derivative... \n", - "5923 WO-2005111625-A3 Method to predict prostate cancer \n", - "6370 US-7868678-B2 Configurable differential lines \n", - "\n", - " content \\\n", - "5611 Technologies and implementations for managing ... \n", - "6895 Compounds of formula I, in which R \n", - "6 Compounds of the formula:\\n[US3946019A] \n", - "5923 A method for predicting the probability or ris... \n", - "6370 Embodiments related to configurable differenti... \n", - "\n", - " ml_generate_embedding_result \\\n", - "5611 [-2.92946529e-02 -1.24640828e-02 1.27173709e-... \n", - "6895 [-6.45397678e-02 1.19616119e-02 -9.85191786e-... \n", - "6 [-3.82784344e-02 -2.31682733e-02 -4.35006060e-... \n", - "5923 [ 0.02480386 -0.01648765 0.03873815 -0.025998... \n", - "6370 [ 2.71715336e-02 -1.93733890e-02 2.82729534e-... \n", - "\n", - " ml_generate_embedding_status \n", - "5611 \n", - "6895 \n", - "6 \n", - "5923 \n", - "6370 \n", - "\n", - "[5 rows x 5 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "Query job 016ad678-9609-4c78-8f07-3f9887ce67ac is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "embedding.head(5)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 53 - }, - "executionInfo": { - "elapsed": 6590, - "status": "ok", - "timestamp": 1742192833667, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "GP3ZqX_bxLGq", - "outputId": "fb823ea2-e47c-415f-84d4-543dd3291e15" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 06ce090b-e3f9-4252-b847-45c2a296ca61 is DONE. 70.9 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "'my_dataset.my_embeddings_table'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# store embeddings in a BQ table\n", - "DATASET_ID = \"my_dataset\" # @param {type:\"string\"}\n", - "TEXT_EMBEDDING_TABLE_ID = \"my_embeddings_table\" # @param {type:\"string\"}\n", - "embedding.to_gbq(f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\", if_exists='replace')" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" + ] + } + ], + "source": [ + "# convert search string to dataframe\n", + "TEXT_SEARCH_DF = bf.DataFrame([TEXT_SEARCH_STRING], columns=['search_string'])\n", + "\n", + "#generate embedding of search query\n", + "search_query = bf.DataFrame(text_model.predict(TEXT_SEARCH_DF))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 104 }, - { - "cell_type": "markdown", - "metadata": { - "id": "OUZ3NNbzo1Tb" - }, - "source": [ - "## Step 2: Indexing and Similarity Search" - ] + "executionInfo": { + "elapsed": 5110, + "status": "ok", + "timestamp": 1742194670801, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "markdown", - "metadata": { - "id": "mvJH2FCmynMm" - }, - "source": [ - "### [Create a Vector Index](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_create_vector_index) using BigFrames\n", - "\n", - "\n", - "**Index Type**\n", - "\n", - "The algorithm to use to build the vector index.\n", - "The supported values are IVF and TREE_AH." - ] + "id": "sx0AGAdn5FYX", + "outputId": "551ebac3-594f-4303-ca97-5301dfee72bb" + }, + "outputs": [], + "source": [ + "## search the base table for the user's query\n", + "\n", + "vector_search_results = bf_bq.vector_search(\n", + " base_table=f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", + " column_to_search=\"ml_generate_embedding_result\",\n", + " query=search_query,\n", + " distance_type=\"cosine\",\n", + " query_column_to_search=\"ml_generate_embedding_result\",\n", + " top_k=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 270 }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "executionInfo": { - "elapsed": 3882, - "status": "ok", - "timestamp": 1742193028877, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "6SBVdv6gyU5A", - "outputId": "6583e113-de27-4b44-972d-c1cc061e3c76" - }, - "outputs": [], - "source": [ - "## create vector index (note only works of tables >5000 rows)\n", - "\n", - "bf_bq.create_vector_index(\n", - " table_id = f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", - " column_name = \"ml_generate_embedding_result\",\n", - " replace= True,\n", - " index_name = \"bf_python_index\",\n", - " distance_type=\"cosine\",\n", - " index_type= \"ivf\"\n", - ")" - ] + "executionInfo": { + "elapsed": 3511, + "status": "ok", + "timestamp": 1742195090670, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "px1v4iJM5L0c", + "outputId": "d107b6e3-a362-42db-c0c2-084d02acd244" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "bo8mBbRLzCOA" - }, - "source": [ - "### Vector Search (semantic search) using Vector Index\n", - "\n", - "ANN (approx nearest neighbor) search using the created vector index" + "data": { + "text/html": [ + "Load job b6b88844-9ed7-4c92-8984-556414592f0b is DONE. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "executionInfo": { - "elapsed": 639, - "status": "ok", - "timestamp": 1742194606771, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "v19BJm_wzPdZ" - }, - "outputs": [], - "source": [ - "## Set variable for vector search\n", - "\n", - "TEXT_SEARCH_STRING = \"Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\" ## replace with whatever search string you want to use for the vector search\n", - "FRACTION_LISTS_TO_SEARCH = 0.01" + "data": { + "text/html": [ + "Query job aa95f59c-7229-4e76-bd2c-3a63deea3285 is DONE. 4.7 kB processed. Open Job" + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 121 - }, - "executionInfo": { - "elapsed": 6927, - "status": "ok", - "timestamp": 1742194625774, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "pAQY1ejpzPap", - "outputId": "485698ad-ac6e-4c93-844e-5d0f30aff13a" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 016ad678-9609-4c78-8f07-3f9887ce67ac is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" - ] - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
querypublication_numbertitle (relevant match)abstract (relevant match)distance
0Chip assemblies employing solder bonds to back...CN-103515336-AChip package, chip arrangement, circuit board ...A chip package is provided, the chip package i...0.287274
0Chip assemblies employing solder bonds to back...US-9548145-B2Microelectronic assembly with multi-layer supp...A method of forming a microelectronic assembly...0.290519
0Chip assemblies employing solder bonds to back...JP-2012074505-ASemiconductor mounting device substrate, semic...To provide a substrate for a semiconductor mou...0.294241
0Chip assemblies employing solder bonds to back...US-2015380164-A1Ceramic electronic componentA ceramic electronic component includes an ele...0.295716
0Chip assemblies employing solder bonds to back...US-2012153447-A1Microelectronic flip chip packages with solder...Processes of assembling microelectronic packag...0.300337
\n", + "

5 rows × 5 columns

\n", + "
[5 rows x 5 columns in total]" ], - "source": [ - "# convert search string to dataframe\n", - "TEXT_SEARCH_DF = bf.DataFrame([TEXT_SEARCH_STRING], columns=['search_string'])\n", - "\n", - "#generate embedding of search query\n", - "search_query = bf.DataFrame(text_model.predict(TEXT_SEARCH_DF))" + "text/plain": [ + " query publication_number \\\n", + "0 Chip assemblies employing solder bonds to back... CN-103515336-A \n", + "0 Chip assemblies employing solder bonds to back... US-9548145-B2 \n", + "0 Chip assemblies employing solder bonds to back... JP-2012074505-A \n", + "0 Chip assemblies employing solder bonds to back... US-2015380164-A1 \n", + "0 Chip assemblies employing solder bonds to back... US-2012153447-A1 \n", + "\n", + " title (relevant match) \\\n", + "0 Chip package, chip arrangement, circuit board ... \n", + "0 Microelectronic assembly with multi-layer supp... \n", + "0 Semiconductor mounting device substrate, semic... \n", + "0 Ceramic electronic component \n", + "0 Microelectronic flip chip packages with solder... \n", + "\n", + " abstract (relevant match) distance \n", + "0 A chip package is provided, the chip package i... 0.287274 \n", + "0 A method of forming a microelectronic assembly... 0.290519 \n", + "0 To provide a substrate for a semiconductor mou... 0.294241 \n", + "0 A ceramic electronic component includes an ele... 0.295716 \n", + "0 Processes of assembling microelectronic packag... 0.300337 \n", + "\n", + "[5 rows x 5 columns]" ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## View the returned results based on simalirity with the user's query\n", + "\n", + "vector_search_results[\n", + " [\n", + " 'content',\n", + " 'publication_number',\n", + " 'title',\n", + " 'content_1',\n", + " 'distance',\n", + " ]\n", + "].rename(columns={\n", + " 'content': 'query',\n", + " 'content_1':'abstract (relevant match)' ,\n", + " 'title':'title (relevant match)',\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "executionInfo": { + "elapsed": 1622, + "status": "ok", + "timestamp": 1742195139318, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 104 - }, - "executionInfo": { - "elapsed": 5110, - "status": "ok", - "timestamp": 1742194670801, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "sx0AGAdn5FYX", - "outputId": "551ebac3-594f-4303-ca97-5301dfee72bb" - }, - "outputs": [], - "source": [ - "## search the base table for the user's query\n", - "\n", - "vector_search_results = bf_bq.vector_search(\n", - " base_table=f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", - " column_to_search=\"ml_generate_embedding_result\",\n", - " query=search_query,\n", - " distance_type=\"cosine\",\n", - " query_column_to_search=\"ml_generate_embedding_result\",\n", - " top_k=5,\n", - ")" - ] + "id": "5fb_O-ne5cvH" + }, + "outputs": [], + "source": [ + "## Brute force result (for comparison)\n", + "\n", + "\n", + "brute_force_result = bf_bq.vector_search(\n", + " base_table=f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", + " column_to_search=\"ml_generate_embedding_result\",\n", + " query=search_query,\n", + " top_k=5,\n", + " distance_type=\"cosine\",\n", + " use_brute_force=True,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "21rNsFMHo8hO" + }, + "source": [ + "## Step 3: AI-Powered Summarization with Retrieval Augmented Generation (RAG)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K3pIQrzB7T_G" + }, + "source": [ + "Patent documents can be dense and time-consuming to digest. AI-Powered Patent Summarization utilizes Retrieval Augmented Generation (RAG) to streamline this process. By retrieving relevant patent information through vector search and then synthesizing it with a large language model, we can generate concise, human-readable summaries, saving valuable time and effort. The code sample below walks through how to set this up continuing with the same user query as the previous use case." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 }, + "executionInfo": { + "elapsed": 4827, + "status": "ok", + "timestamp": 1742195565658, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 + }, + "id": "jb5rueqU7T5J", + "outputId": "43732836-ebae-4fb3-b28e-bfea51146c72" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 270 - }, - "executionInfo": { - "elapsed": 3511, - "status": "ok", - "timestamp": 1742195090670, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "px1v4iJM5L0c", - "outputId": "d107b6e3-a362-42db-c0c2-084d02acd244" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Load job b6b88844-9ed7-4c92-8984-556414592f0b is DONE. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job aa95f59c-7229-4e76-bd2c-3a63deea3285 is DONE. 4.7 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
querypublication_numbertitle (relevant match)abstract (relevant match)distance
0Chip assemblies employing solder bonds to back...CN-103515336-AChip package, chip arrangement, circuit board ...A chip package is provided, the chip package i...0.287274
0Chip assemblies employing solder bonds to back...US-9548145-B2Microelectronic assembly with multi-layer supp...A method of forming a microelectronic assembly...0.290519
0Chip assemblies employing solder bonds to back...JP-2012074505-ASemiconductor mounting device substrate, semic...To provide a substrate for a semiconductor mou...0.294241
0Chip assemblies employing solder bonds to back...US-2015380164-A1Ceramic electronic componentA ceramic electronic component includes an ele...0.295716
0Chip assemblies employing solder bonds to back...US-2012153447-A1Microelectronic flip chip packages with solder...Processes of assembling microelectronic packag...0.300337
\n", - "

5 rows × 5 columns

\n", - "
[5 rows x 5 columns in total]" - ], - "text/plain": [ - " query publication_number \\\n", - "0 Chip assemblies employing solder bonds to back... CN-103515336-A \n", - "0 Chip assemblies employing solder bonds to back... US-9548145-B2 \n", - "0 Chip assemblies employing solder bonds to back... JP-2012074505-A \n", - "0 Chip assemblies employing solder bonds to back... US-2015380164-A1 \n", - "0 Chip assemblies employing solder bonds to back... US-2012153447-A1 \n", - "\n", - " title (relevant match) \\\n", - "0 Chip package, chip arrangement, circuit board ... \n", - "0 Microelectronic assembly with multi-layer supp... \n", - "0 Semiconductor mounting device substrate, semic... \n", - "0 Ceramic electronic component \n", - "0 Microelectronic flip chip packages with solder... \n", - "\n", - " abstract (relevant match) distance \n", - "0 A chip package is provided, the chip package i... 0.287274 \n", - "0 A method of forming a microelectronic assembly... 0.290519 \n", - "0 To provide a substrate for a semiconductor mou... 0.294241 \n", - "0 A ceramic electronic component includes an ele... 0.295716 \n", - "0 Processes of assembling microelectronic packag... 0.300337 \n", - "\n", - "[5 rows x 5 columns]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "Query job 3fabe659-f95b-49cb-b0c7-9d32b09177bf is DONE. 0 Bytes processed. Open Job" ], - "source": [ - "## View the returned results based on simalirity with the user's query\n", - "\n", - "vector_search_results[\n", - " [\n", - " 'content',\n", - " 'publication_number',\n", - " 'title',\n", - " 'content_1',\n", - " 'distance',\n", - " ]\n", - "].rename(columns={\n", - " 'content': 'query',\n", - " 'content_1':'abstract (relevant match)' ,\n", - " 'title':'title (relevant match)',\n", - "})" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "## gemini model\n", + "\n", + "llm_model = bf_llm.GeminiTextGenerator(model_name = \"gemini-2.0-flash-001\") ## replace with other model as needed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "41e12JTf70sr" + }, + "source": [ + "We will use the same user query from Section 2, and pass the list of abstracts returned by the vector search into the prompt for the RAG application" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "executionInfo": { + "elapsed": 1474, + "status": "ok", + "timestamp": 1742195536109, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "executionInfo": { - "elapsed": 1622, - "status": "ok", - "timestamp": 1742195139318, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "5fb_O-ne5cvH" - }, - "outputs": [], - "source": [ - "## Brute force result (for comparison)\n", - "\n", - "\n", - "brute_force_result = bf_bq.vector_search(\n", - " base_table=f\"{DATASET_ID}.{TEXT_EMBEDDING_TABLE_ID}\",\n", - " column_to_search=\"ml_generate_embedding_result\",\n", - " query=search_query,\n", - " top_k=5,\n", - " distance_type=\"cosine\",\n", - " use_brute_force=True,\n", - ")\n" - ] + "id": "EyP-ZFJK8h-2" + }, + "outputs": [], + "source": [ + "TEMPERATURE = 0.4" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 72 }, - { - "cell_type": "markdown", - "metadata": { - "id": "21rNsFMHo8hO" - }, - "source": [ - "## Step 3: AI-Powered Summarization with Retrieval Augmented Generation (RAG)" - ] + "executionInfo": { + "elapsed": 3371, + "status": "ok", + "timestamp": 1742195421813, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "eP99R6SV7Tug", + "outputId": "c34bc931-5be8-410e-ac1f-604df31ef533" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "K3pIQrzB7T_G" - }, - "source": [ - "Patent documents can be dense and time-consuming to digest. AI-Powered Patent Summarization utilizes Retrieval Augmented Generation (RAG) to streamline this process. By retrieving relevant patent information through vector search and then synthesizing it with a large language model, we can generate concise, human-readable summaries, saving valuable time and effort. The code sample below walks through how to set this up continuing with the same user query as the previous use case." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "['{\"abstract\": \"A chip package is provided, the chip package including: a chip carrier; a chip disposed over and electrically connected to a chip carrier top side; an electrically insulating material disposed over and at least partially surrounding the chip; one or more electrically conductive contact regions formed over the electrically insulating material and in electrical connection with the chip; and another electrically insulating material disposed over a chip carrier bottom side. An electrically conductive contact region on the chip carrier bottom side is released from the further electrically insulating material.\"}', '{\"abstract\": \"A method of forming a microelectronic assembly includes positioning a support structure adjacent to an active region of a device but not extending onto the active region. The support structure has planar sections. Each planar section has a substantially uniform composition. The composition of at least one of the planar sections differs from the composition of at least one of the other planar sections. A lid is positioned in contact with the support structure and extends over the active region. The support structure is bonded to the device and to the lid.\"}', '{\"abstract\": \"To provide a substrate for a semiconductor mounting device capable of obtaining high reliability. In a semiconductor mounting device substrate of the present invention, a semiconductor chip can be surface-mounted by a flip chip connection method on a semiconductor chip mounting region of a first main surface of a multilayer wiring substrate. A plurality of second main surface side solder bumps 52 forming a plate-like component mounting region 53 are formed at a location immediately below the semiconductor chip 21 on the second main surface 13 of the multilayer wiring board 11. A plate-like component 101 mainly composed of an inorganic material is surface-mounted on the multilayer wiring board 11 by a flip chip connection method via a plurality of second main surface side solder bumps 52. A plurality of second main surface side solder bumps 52 are sealed by a second main surface side underfill 107 provided in the gap S <b> 2 between the second main surface 13 and the plate-like component 101. [Selection] Figure 1\"}', '{\"abstract\": \"A ceramic electronic component includes an electronic component body, an inner electrode, and an outer electrode. The outer electrode includes a fired electrode layer and first and second plated layers. The fired electrode layer is disposed on the electronic component body. The first plated layer is disposed on the fired electrode layer. The thickness of the first plated layer is about 3 \\\\u03bcm to about 8 \\\\u03bcm, for example. The first plated layer contains nickel. The second plated layer is disposed on the first plated layer. The thickness of the second plated layer is about 0.025 \\\\u03bcm to about 1 \\\\u03bcm, for example. The second plated layer contains lead.\"}', '{\"abstract\": \"Processes of assembling microelectronic packages with lead frames and/or other suitable substrates are described herein. In one embodiment, a method for fabricating a semiconductor assembly includes forming an attachment area and a non-attachment area on a lead finger of a lead frame. The attachment area is more wettable to the solder ball than the non-attachment area during reflow. The method also includes contacting a solder ball carried by a semiconductor die with the attachment area of the lead finger, reflowing the solder ball while the solder ball is in contact with the attachment area of the lead finger, and controllably collapsing the solder ball to establish an electrical connection between the semiconductor die and the lead finger of the lead frame.\"}']\n" + ] + } + ], + "source": [ + "# Extract strings into a list of JSON strings\n", + "json_strings = [json.dumps({'abstract': s}) for s in vector_search_results['content_1']]\n", + "ALL_ABSTRACTS = json_strings\n", + "\n", + "# Print the result (optional)\n", + "print(ALL_ABSTRACTS)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "executionInfo": { - "elapsed": 4827, - "status": "ok", - "timestamp": 1742195565658, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "jb5rueqU7T5J", - "outputId": "43732836-ebae-4fb3-b28e-bfea51146c72" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 3fabe659-f95b-49cb-b0c7-9d32b09177bf is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "## gemini model\n", - "\n", - "llm_model = bf_llm.GeminiTextGenerator(model_name = \"gemini-2.0-flash-001\") ## replace with other model as needed" - ] + "collapsed": true, + "executionInfo": { + "elapsed": 1620, + "status": "ok", + "timestamp": 1742195587180, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "kSNSi1GV8OAD", + "outputId": "37fbc822-1160-4fbd-c7d6-ecb4a16db394" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "41e12JTf70sr" - }, - "source": [ - "We will use the same user query from Section 2, and pass the list of abstracts returned by the vector search into the prompt for the RAG application" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "You are an expert patent analyst. I will provide you the abstracts of the top 5 patents in json format retrieved by a vector search based on a user's query.\n", + "Your task is to analyze these abstracts and generate a concise, coherent summary that encapsulates the core innovations and concepts shared among them.\n", + "\n", + "In your output, share the original user query.\n", + "Then output the concise, coherent summary that encapsulates the core innovations and concepts shared among the top 5 abstracts. The heading for this section should\n", + "be : Summary of the top 5 abstracts that are semantically closest to the user query.\n", + "\n", + "User Query: Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\n", + "Top 5 abstracts: ['{\"abstract\": \"A chip package is provided, the chip package including: a chip carrier; a chip disposed over and electrically connected to a chip carrier top side; an electrically insulating material disposed over and at least partially surrounding the chip; one or more electrically conductive contact regions formed over the electrically insulating material and in electrical connection with the chip; and another electrically insulating material disposed over a chip carrier bottom side. An electrically conductive contact region on the chip carrier bottom side is released from the further electrically insulating material.\"}', '{\"abstract\": \"A method of forming a microelectronic assembly includes positioning a support structure adjacent to an active region of a device but not extending onto the active region. The support structure has planar sections. Each planar section has a substantially uniform composition. The composition of at least one of the planar sections differs from the composition of at least one of the other planar sections. A lid is positioned in contact with the support structure and extends over the active region. The support structure is bonded to the device and to the lid.\"}', '{\"abstract\": \"To provide a substrate for a semiconductor mounting device capable of obtaining high reliability. In a semiconductor mounting device substrate of the present invention, a semiconductor chip can be surface-mounted by a flip chip connection method on a semiconductor chip mounting region of a first main surface of a multilayer wiring substrate. A plurality of second main surface side solder bumps 52 forming a plate-like component mounting region 53 are formed at a location immediately below the semiconductor chip 21 on the second main surface 13 of the multilayer wiring board 11. A plate-like component 101 mainly composed of an inorganic material is surface-mounted on the multilayer wiring board 11 by a flip chip connection method via a plurality of second main surface side solder bumps 52. A plurality of second main surface side solder bumps 52 are sealed by a second main surface side underfill 107 provided in the gap S <b> 2 between the second main surface 13 and the plate-like component 101. [Selection] Figure 1\"}', '{\"abstract\": \"A ceramic electronic component includes an electronic component body, an inner electrode, and an outer electrode. The outer electrode includes a fired electrode layer and first and second plated layers. The fired electrode layer is disposed on the electronic component body. The first plated layer is disposed on the fired electrode layer. The thickness of the first plated layer is about 3 \\\\u03bcm to about 8 \\\\u03bcm, for example. The first plated layer contains nickel. The second plated layer is disposed on the first plated layer. The thickness of the second plated layer is about 0.025 \\\\u03bcm to about 1 \\\\u03bcm, for example. The second plated layer contains lead.\"}', '{\"abstract\": \"Processes of assembling microelectronic packages with lead frames and/or other suitable substrates are described herein. In one embodiment, a method for fabricating a semiconductor assembly includes forming an attachment area and a non-attachment area on a lead finger of a lead frame. The attachment area is more wettable to the solder ball than the non-attachment area during reflow. The method also includes contacting a solder ball carried by a semiconductor die with the attachment area of the lead finger, reflowing the solder ball while the solder ball is in contact with the attachment area of the lead finger, and controllably collapsing the solder ball to establish an electrical connection between the semiconductor die and the lead finger of the lead frame.\"}']\n", + "\n", + "Instructions:\n", + "\n", + "Focus on identifying the common themes and key technological advancements described in the abstracts.\n", + "Synthesize the information into a clear and concise summary, approximately 150-200 words.\n", + "Avoid simply copying phrases from the abstracts. Instead, aim to provide a cohesive overview of the shared concepts.\n", + "Highlight the potential applications and benefits of the described inventions.\n", + "Maintain a professional and objective tone.\n", + "Do not mention the individual patents by number, focus on summarizing the shared concepts.\n", + "\n" + ] + } + ], + "source": [ + "## Setup the LLM prompt\n", + "\n", + "prompt = f\"\"\"\n", + "You are an expert patent analyst. I will provide you the abstracts of the top 5 patents in json format retrieved by a vector search based on a user's query.\n", + "Your task is to analyze these abstracts and generate a concise, coherent summary that encapsulates the core innovations and concepts shared among them.\n", + "\n", + "In your output, share the original user query.\n", + "Then output the concise, coherent summary that encapsulates the core innovations and concepts shared among the top 5 abstracts. The heading for this section should\n", + "be : Summary of the top 5 abstracts that are semantically closest to the user query.\n", + "\n", + "User Query: {TEXT_SEARCH_STRING}\n", + "Top 5 abstracts: {ALL_ABSTRACTS}\n", + "\n", + "Instructions:\n", + "\n", + "Focus on identifying the common themes and key technological advancements described in the abstracts.\n", + "Synthesize the information into a clear and concise summary, approximately 150-200 words.\n", + "Avoid simply copying phrases from the abstracts. Instead, aim to provide a cohesive overview of the shared concepts.\n", + "Highlight the potential applications and benefits of the described inventions.\n", + "Maintain a professional and objective tone.\n", + "Do not mention the individual patents by number, focus on summarizing the shared concepts.\n", + "\"\"\"\n", + "\n", + "print(prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "executionInfo": { + "elapsed": 1, + "status": "ok", + "timestamp": 1742195567707, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "executionInfo": { - "elapsed": 1474, - "status": "ok", - "timestamp": 1742195536109, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "EyP-ZFJK8h-2" - }, - "outputs": [], - "source": [ - "TEMPERATURE = 0.4" - ] + "id": "njiQdfkT8Y7V" + }, + "outputs": [], + "source": [ + "## Define a function that will take the input propmpt and run the LLM\n", + "\n", + "def predict(prompt: str, temperature: float = TEMPERATURE) -> str:\n", + " # Create dataframe\n", + " input = bf.DataFrame(\n", + " {\n", + " \"prompt\": [prompt],\n", + " }\n", + " )\n", + "\n", + " # Return response\n", + " return llm_model.predict(input, temperature=temperature).ml_generate_text_llm_result.iloc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 426 + }, + "executionInfo": { + "elapsed": 14425, + "status": "ok", + "timestamp": 1742195608280, + "user": { + "displayName": "", + "userId": "" + }, + "user_tz": -480 }, + "id": "OYYkVYbs8Y0P", + "outputId": "def839e3-3dee-4320-9cb5-cac855ddea6b" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 72 - }, - "executionInfo": { - "elapsed": 3371, - "status": "ok", - "timestamp": 1742195421813, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "eP99R6SV7Tug", - "outputId": "c34bc931-5be8-410e-ac1f-604df31ef533" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['{\"abstract\": \"A chip package is provided, the chip package including: a chip carrier; a chip disposed over and electrically connected to a chip carrier top side; an electrically insulating material disposed over and at least partially surrounding the chip; one or more electrically conductive contact regions formed over the electrically insulating material and in electrical connection with the chip; and another electrically insulating material disposed over a chip carrier bottom side. An electrically conductive contact region on the chip carrier bottom side is released from the further electrically insulating material.\"}', '{\"abstract\": \"A method of forming a microelectronic assembly includes positioning a support structure adjacent to an active region of a device but not extending onto the active region. The support structure has planar sections. Each planar section has a substantially uniform composition. The composition of at least one of the planar sections differs from the composition of at least one of the other planar sections. A lid is positioned in contact with the support structure and extends over the active region. The support structure is bonded to the device and to the lid.\"}', '{\"abstract\": \"To provide a substrate for a semiconductor mounting device capable of obtaining high reliability. In a semiconductor mounting device substrate of the present invention, a semiconductor chip can be surface-mounted by a flip chip connection method on a semiconductor chip mounting region of a first main surface of a multilayer wiring substrate. A plurality of second main surface side solder bumps 52 forming a plate-like component mounting region 53 are formed at a location immediately below the semiconductor chip 21 on the second main surface 13 of the multilayer wiring board 11. A plate-like component 101 mainly composed of an inorganic material is surface-mounted on the multilayer wiring board 11 by a flip chip connection method via a plurality of second main surface side solder bumps 52. A plurality of second main surface side solder bumps 52 are sealed by a second main surface side underfill 107 provided in the gap S <b> 2 between the second main surface 13 and the plate-like component 101. [Selection] Figure 1\"}', '{\"abstract\": \"A ceramic electronic component includes an electronic component body, an inner electrode, and an outer electrode. The outer electrode includes a fired electrode layer and first and second plated layers. The fired electrode layer is disposed on the electronic component body. The first plated layer is disposed on the fired electrode layer. The thickness of the first plated layer is about 3 \\\\u03bcm to about 8 \\\\u03bcm, for example. The first plated layer contains nickel. The second plated layer is disposed on the first plated layer. The thickness of the second plated layer is about 0.025 \\\\u03bcm to about 1 \\\\u03bcm, for example. The second plated layer contains lead.\"}', '{\"abstract\": \"Processes of assembling microelectronic packages with lead frames and/or other suitable substrates are described herein. In one embodiment, a method for fabricating a semiconductor assembly includes forming an attachment area and a non-attachment area on a lead finger of a lead frame. The attachment area is more wettable to the solder ball than the non-attachment area during reflow. The method also includes contacting a solder ball carried by a semiconductor die with the attachment area of the lead finger, reflowing the solder ball while the solder ball is in contact with the attachment area of the lead finger, and controllably collapsing the solder ball to establish an electrical connection between the semiconductor die and the lead finger of the lead frame.\"}']\n" - ] - } + "data": { + "text/html": [ + "Load job 34f3b649-6e45-46db-a6e5-405ae0a8bf69 is DONE. Open Job" ], - "source": [ - "# Extract strings into a list of JSON strings\n", - "json_strings = [json.dumps({'abstract': s}) for s in vector_search_results['content_1']]\n", - "ALL_ABSTRACTS = json_strings\n", - "\n", - "# Print the result (optional)\n", - "print(ALL_ABSTRACTS)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "collapsed": true, - "executionInfo": { - "elapsed": 1620, - "status": "ok", - "timestamp": 1742195587180, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "kSNSi1GV8OAD", - "outputId": "37fbc822-1160-4fbd-c7d6-ecb4a16db394" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "You are an expert patent analyst. I will provide you the abstracts of the top 5 patents in json format retrieved by a vector search based on a user's query.\n", - "Your task is to analyze these abstracts and generate a concise, coherent summary that encapsulates the core innovations and concepts shared among them.\n", - "\n", - "In your output, share the original user query.\n", - "Then output the concise, coherent summary that encapsulates the core innovations and concepts shared among the top 5 abstracts. The heading for this section should\n", - "be : Summary of the top 5 abstracts that are semantically closest to the user query.\n", - "\n", - "User Query: Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\n", - "Top 5 abstracts: ['{\"abstract\": \"A chip package is provided, the chip package including: a chip carrier; a chip disposed over and electrically connected to a chip carrier top side; an electrically insulating material disposed over and at least partially surrounding the chip; one or more electrically conductive contact regions formed over the electrically insulating material and in electrical connection with the chip; and another electrically insulating material disposed over a chip carrier bottom side. An electrically conductive contact region on the chip carrier bottom side is released from the further electrically insulating material.\"}', '{\"abstract\": \"A method of forming a microelectronic assembly includes positioning a support structure adjacent to an active region of a device but not extending onto the active region. The support structure has planar sections. Each planar section has a substantially uniform composition. The composition of at least one of the planar sections differs from the composition of at least one of the other planar sections. A lid is positioned in contact with the support structure and extends over the active region. The support structure is bonded to the device and to the lid.\"}', '{\"abstract\": \"To provide a substrate for a semiconductor mounting device capable of obtaining high reliability. In a semiconductor mounting device substrate of the present invention, a semiconductor chip can be surface-mounted by a flip chip connection method on a semiconductor chip mounting region of a first main surface of a multilayer wiring substrate. A plurality of second main surface side solder bumps 52 forming a plate-like component mounting region 53 are formed at a location immediately below the semiconductor chip 21 on the second main surface 13 of the multilayer wiring board 11. A plate-like component 101 mainly composed of an inorganic material is surface-mounted on the multilayer wiring board 11 by a flip chip connection method via a plurality of second main surface side solder bumps 52. A plurality of second main surface side solder bumps 52 are sealed by a second main surface side underfill 107 provided in the gap S <b> 2 between the second main surface 13 and the plate-like component 101. [Selection] Figure 1\"}', '{\"abstract\": \"A ceramic electronic component includes an electronic component body, an inner electrode, and an outer electrode. The outer electrode includes a fired electrode layer and first and second plated layers. The fired electrode layer is disposed on the electronic component body. The first plated layer is disposed on the fired electrode layer. The thickness of the first plated layer is about 3 \\\\u03bcm to about 8 \\\\u03bcm, for example. The first plated layer contains nickel. The second plated layer is disposed on the first plated layer. The thickness of the second plated layer is about 0.025 \\\\u03bcm to about 1 \\\\u03bcm, for example. The second plated layer contains lead.\"}', '{\"abstract\": \"Processes of assembling microelectronic packages with lead frames and/or other suitable substrates are described herein. In one embodiment, a method for fabricating a semiconductor assembly includes forming an attachment area and a non-attachment area on a lead finger of a lead frame. The attachment area is more wettable to the solder ball than the non-attachment area during reflow. The method also includes contacting a solder ball carried by a semiconductor die with the attachment area of the lead finger, reflowing the solder ball while the solder ball is in contact with the attachment area of the lead finger, and controllably collapsing the solder ball to establish an electrical connection between the semiconductor die and the lead finger of the lead frame.\"}']\n", - "\n", - "Instructions:\n", - "\n", - "Focus on identifying the common themes and key technological advancements described in the abstracts.\n", - "Synthesize the information into a clear and concise summary, approximately 150-200 words.\n", - "Avoid simply copying phrases from the abstracts. Instead, aim to provide a cohesive overview of the shared concepts.\n", - "Highlight the potential applications and benefits of the described inventions.\n", - "Maintain a professional and objective tone.\n", - "Do not mention the individual patents by number, focus on summarizing the shared concepts.\n", - "\n" - ] - } + "data": { + "text/html": [ + "Query job a574725f-64ae-4a19-aac0-959bec0bffeb is DONE. 5.0 kB processed. Open Job" ], - "source": [ - "## Setup the LLM prompt\n", - "\n", - "prompt = f\"\"\"\n", - "You are an expert patent analyst. I will provide you the abstracts of the top 5 patents in json format retrieved by a vector search based on a user's query.\n", - "Your task is to analyze these abstracts and generate a concise, coherent summary that encapsulates the core innovations and concepts shared among them.\n", - "\n", - "In your output, share the original user query.\n", - "Then output the concise, coherent summary that encapsulates the core innovations and concepts shared among the top 5 abstracts. The heading for this section should\n", - "be : Summary of the top 5 abstracts that are semantically closest to the user query.\n", - "\n", - "User Query: {TEXT_SEARCH_STRING}\n", - "Top 5 abstracts: {ALL_ABSTRACTS}\n", - "\n", - "Instructions:\n", - "\n", - "Focus on identifying the common themes and key technological advancements described in the abstracts.\n", - "Synthesize the information into a clear and concise summary, approximately 150-200 words.\n", - "Avoid simply copying phrases from the abstracts. Instead, aim to provide a cohesive overview of the shared concepts.\n", - "Highlight the potential applications and benefits of the described inventions.\n", - "Maintain a professional and objective tone.\n", - "Do not mention the individual patents by number, focus on summarizing the shared concepts.\n", - "\"\"\"\n", - "\n", - "print(prompt)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "executionInfo": { - "elapsed": 1, - "status": "ok", - "timestamp": 1742195567707, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "njiQdfkT8Y7V" - }, - "outputs": [], - "source": [ - "## Define a function that will take the input propmpt and run the LLM\n", - "\n", - "def predict(prompt: str, temperature: float = TEMPERATURE) -> str:\n", - " # Create dataframe\n", - " input = bf.DataFrame(\n", - " {\n", - " \"prompt\": [prompt],\n", - " }\n", - " )\n", - "\n", - " # Return response\n", - " return llm_model.predict(input, temperature=temperature).ml_generate_text_llm_result.iloc[0]" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" + ] }, { - "cell_type": "code", - "execution_count": 25, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 426 - }, - "executionInfo": { - "elapsed": 14425, - "status": "ok", - "timestamp": 1742195608280, - "user": { - "displayName": "", - "userId": "" - }, - "user_tz": -480 - }, - "id": "OYYkVYbs8Y0P", - "outputId": "def839e3-3dee-4320-9cb5-cac855ddea6b" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Load job 34f3b649-6e45-46db-a6e5-405ae0a8bf69 is DONE. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job a574725f-64ae-4a19-aac0-959bec0bffeb is DONE. 5.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes-2/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" - ] - }, - { - "data": { - "text/markdown": [ - "User Query: Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\n", - "\n", - "Summary of the top 5 abstracts that are semantically closest to the user query:\n", - "\n", - "The abstracts describe various aspects of microelectronic assembly and packaging, with a focus on enhancing reliability and electrical connectivity. A common theme is the use of solder bumps or balls for creating electrical connections between different components, such as semiconductor chips and substrates or lead frames. Several abstracts highlight methods for improving the solderability and wettability of contact regions, often involving the use of multiple layers with differing compositions. The use of electrically insulating materials to provide support and protection to the chip and electrical connections is also described. One abstract specifically mentions a nickel-containing plated layer as part of an outer electrode, suggesting its role in improving the electrical or mechanical properties of the connection. The innovations aim to improve the reliability and performance of microelectronic devices through optimized material selection, assembly processes, and structural designs.\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/markdown": [ + "User Query: Chip assemblies employing solder bonds to back-side lands including an electrolytic nickel layer\n", + "\n", + "Summary of the top 5 abstracts that are semantically closest to the user query:\n", + "\n", + "The abstracts describe various aspects of microelectronic assembly and packaging, with a focus on enhancing reliability and electrical connectivity. A common theme is the use of solder bumps or balls for creating electrical connections between different components, such as semiconductor chips and substrates or lead frames. Several abstracts highlight methods for improving the solderability and wettability of contact regions, often involving the use of multiple layers with differing compositions. The use of electrically insulating materials to provide support and protection to the chip and electrical connections is also described. One abstract specifically mentions a nickel-containing plated layer as part of an outer electrode, suggesting its role in improving the electrical or mechanical properties of the connection. The innovations aim to improve the reliability and performance of microelectronic devices through optimized material selection, assembly processes, and structural designs.\n" ], - "source": [ - "# Invoke LLM with prompt\n", - "response = predict(prompt, temperature = TEMPERATURE)\n", - "\n", - "# Print results as Markdown\n", - "Markdown(response)" + "text/plain": [ + "" ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sy82XLDfooEb" - }, - "source": [ - "# Summary and next steps\n", - "\n", - "Ready to dive deeper and explore the endless possibilities? Start building your own vector search applications with BigFrames and BigQuery today! Check out our [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_vector_search), explore our sample [notebooks](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks), and unleash the power of vector analytics on your data.\n", - "The BigFrames team would also love to hear from you. If you would like to reach out, please send an email to: bigframes-feedback@google.com or by filing an issue at the [open source BigFrames repository](https://github.com/googleapis/python-bigquery-dataframes/issues). To receive updates about BigFrames, subscribe to the BigFrames email list." - ] - } - ], - "metadata": { - "colab": { - "name": "bq_dataframes_llm_kmeans", - "provenance": [], - "toc_visible": true - }, - "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.10.16" + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" } + ], + "source": [ + "# Invoke LLM with prompt\n", + "response = predict(prompt, temperature = TEMPERATURE)\n", + "\n", + "# Print results as Markdown\n", + "Markdown(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sy82XLDfooEb" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "Ready to dive deeper and explore the endless possibilities? Start building your own vector search applications with BigFrames and BigQuery today! Check out our [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.bigquery#bigframes_bigquery_vector_search), explore our sample [notebooks](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks), and unleash the power of vector analytics on your data.\n", + "The BigFrames team would also love to hear from you. If you would like to reach out, please send an email to: bigframes-feedback@google.com or by filing an issue at the [open source BigFrames repository](https://github.com/googleapis/python-bigquery-dataframes/issues). To receive updates about BigFrames, subscribe to the BigFrames email list." + ] + } + ], + "metadata": { + "colab": { + "name": "bq_dataframes_llm_kmeans", + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "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.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/generative_ai/large_language_models.ipynb b/notebooks/generative_ai/large_language_models.ipynb index 1d7bc7f6ef1..e064394c4bd 100644 --- a/notebooks/generative_ai/large_language_models.ipynb +++ b/notebooks/generative_ai/large_language_models.ipynb @@ -16,7 +16,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Define the model" + "# Define the model" ] }, { diff --git a/notebooks/geo/geoseries.ipynb b/notebooks/geo/geoseries.ipynb index 953fc8f45fa..1159b8d31de 100644 --- a/notebooks/geo/geoseries.ipynb +++ b/notebooks/geo/geoseries.ipynb @@ -44,7 +44,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 1. Load the Counties table from the Census Bureau US Boundaries dataset" + "## 1. Load the Counties table from the Census Bureau US Boundaries dataset" ] }, { @@ -699,7 +699,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Reuse `five_geom` and `geom_obj` to find the difference between the geometry objects" + "### Reuse `five_geom` and `geom_obj` to find the difference between the geometry objects" ] }, { @@ -902,7 +902,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Reuse `wkts_from_geo` and `geom_obj`" + "### Reuse `wkts_from_geo` and `geom_obj`" ] }, { diff --git a/notebooks/getting_started/pandas_extensions.ipynb b/notebooks/getting_started/pandas_extensions.ipynb new file mode 100644 index 00000000000..c511eab9b4a --- /dev/null +++ b/notebooks/getting_started/pandas_extensions.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BigQuery extension for pandas\n", + "\n", + "BigQuery DataFrames provides a pandas extension to execute BigQuery SQL scalar functions directly on pandas DataFrames." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import bigframes # This import registers the bigquery accessor." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, BigQuery DataFrames selects a location to process data based on the\n", + "data location, but using a pandas object doesn't provide such informat. If\n", + "processing location is important to you, configure the location before using the\n", + "accessor." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "\n", + "bpd.reset_session()\n", + "bpd.options.bigquery.location = \"US\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using `sql_scalar`\n", + "\n", + "The `bigquery.sql_scalar` method allows you to apply a SQL scalar function to a pandas DataFrame by converting it to BigFrames, executing the SQL in BigQuery, and returning the result as a pandas Series." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " Query processed 0 Bytes in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 2.0\n", + "1 3.0\n", + "2 4.0\n", + "dtype: Float64" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame({\"a\": [1.5, 2.5, 3.5]})\n", + "result = df.bigquery.sql_scalar(\"ROUND({0}, 0)\")\n", + "result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also use multiple columns." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " Query processed 0 Bytes in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 11\n", + "1 22\n", + "2 33\n", + "dtype: Int64" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame({\"a\": [1, 2, 3], \"b\": [10, 20, 30]})\n", + "result = df.bigquery.sql_scalar(\"{a} + {b}\")\n", + "result" + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/location/regionalized.ipynb b/notebooks/location/regionalized.ipynb index 23313ec0c4c..b1e9e010d48 100644 --- a/notebooks/location/regionalized.ipynb +++ b/notebooks/location/regionalized.ipynb @@ -17,7 +17,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Infer location and set up data in that location if needed" + "## Infer location and set up data in that location if needed" ] }, { @@ -126,7 +126,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Set BigQuery DataFrames options" + "## Set BigQuery DataFrames options" ] }, { @@ -1344,7 +1344,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### BigQuery DataFrames gives you the ability to turn your custom scalar functions into a BigQuery remote function.\n", + "## BigQuery DataFrames gives you the ability to turn your custom scalar functions into a BigQuery remote function.", "\n", "It requires the GCP project to be set up appropriately and the user having sufficient privileges to use them. One can find more details on it via `help` command." ] @@ -1643,7 +1643,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Initialize a DataFrame from a BigQuery table" + "## Initialize a DataFrame from a BigQuery table" ] }, { diff --git a/notebooks/ml/bq_dataframes_ml_linear_regression.ipynb b/notebooks/ml/bq_dataframes_ml_linear_regression.ipynb index 00aa7a347cb..210922eab94 100644 --- a/notebooks/ml/bq_dataframes_ml_linear_regression.ipynb +++ b/notebooks/ml/bq_dataframes_ml_linear_regression.ipynb @@ -1,760 +1,760 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2023 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": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "## Train a linear regression model with BigQuery DataFrames ML\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "24743cf4a1e1" - }, - "source": [ - "**_NOTE_**: This notebook has been tested in the following environment:\n", - "\n", - "* Python version = 3.10" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "Use this notebook to learn how to train a linear regression model using BigQuery DataFrames ML. BigQuery DataFrames ML provides a provides a scikit-learn-like API for ML powered by the BigQuery engine.\n", - "\n", - "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial).\n", - "\n", - "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you use BigQuery DataFrames to create a linear regression model that predicts the weight of an Adelie penguin based on the penguin's island of residence, culmen length and depth, flipper length, and sex.\n", - "\n", - "The steps include:\n", - "\n", - "- Creating a DataFrame from a BigQuery table.\n", - "- Cleaning and preparing data using pandas.\n", - "- Creating a linear regression model using `bigframes.ml`.\n", - "- Saving the ML model to BigQuery for future use." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery Public Dataset) which includes data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "* BigQuery ML\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", - "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", - "\n", - "1. Install the package\n", - "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9O0Ka4W2MNF3" - }, - "outputs": [], - "source": [ - "# !pip install bigframes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "# import IPython\n", - "\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "oDfTjfACBvJk" - }, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "If you don't know your project ID, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "eF-Twtc4XGem" - }, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bpd" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "### Set BigQuery DataFrames options" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NPPMuw2PXGeo" - }, - "outputs": [], - "source": [ - "# Note: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bpd.options.bigquery.project = PROJECT_ID\n", - "\n", - "# Note: The location option is not required.\n", - "# It defaults to the location of the first table or query\n", - "# passed to read_gbq(). For APIs where a location can't be\n", - "# auto-detected, the location defaults to the \"US\" location.\n", - "bpd.options.bigquery.location = REGION" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "D21CoOlfFTYI" - }, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9EMAqR37AfLS" - }, - "source": [ - "## Read a BigQuery table into a BigQuery DataFrames DataFrame\n", - "\n", - "Read the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) into a BigQuery DataFrames DataFrame:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "EDAaIwHpQCDZ" - }, - "outputs": [], - "source": [ - "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DJu837YEXD7B" - }, - "source": [ - "Take a look at the DataFrame:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "_gPD0Zn1Stdb" - }, - "outputs": [], - "source": [ - "df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rwPLjqW2Ajzh" - }, - "source": [ - "## Clean and prepare data\n", - "\n", - "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment.\n", - "\n", - "Because this model will focus on the Adelie Penguin species, you need to filter the data for only those rows representing Adelie penguins. Then you drop the `species` column because it is no longer needed.\n", - "\n", - "As these functions are applied, only the new DataFrame object `adelie_data` is modified. The source table and the original DataFrame object `df` don't change." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "6i6HkFJZa8na" - }, - "outputs": [], - "source": [ - "# Filter down to the data to the Adelie Penguin species\n", - "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", - "\n", - "# Drop the species column\n", - "adelie_data = adelie_data.drop(columns=[\"species\"])\n", - "\n", - "# Take a look at the filtered DataFrame\n", - "adelie_data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "jhK2OlyMbY4L" - }, - "source": [ - "Drop rows with `NULL` values in order to create a BigQuery DataFrames DataFrame for the training data:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "0am3hdlXZfxZ" - }, - "outputs": [], - "source": [ - "# Drop rows with nulls to get training data\n", - "training_data = adelie_data.dropna()\n", - "\n", - "# Take a peek at the training data\n", - "training_data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "M_-0X7NxYK5f" - }, - "source": [ - "Specify your feature (or input) columns and the label (or output) column:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "YKwCW7Nsavap" - }, - "outputs": [], - "source": [ - "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", - "label_columns = training_data[['body_mass_g']]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "CjyM7vZJZ0sQ" - }, - "source": [ - "There is a row within the `adelie_data` BigQuery DataFrames DataFrame that has a `NULL` value for the `body mass` column. `body mass` is the label column, which is the value that the model you are creating is trying to predict.\n", - "\n", - "Create a new BigQuery DataFrames DataFrame, `test_data`, for this row so that you can use it as test data on which to make a prediction later:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "wej78IDUaRW9" - }, - "outputs": [], - "source": [ - "test_data = adelie_data[adelie_data.body_mass_g.isnull()]\n", - "\n", - "test_data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Fx4lsNqMorJ-" - }, - "source": [ - "## Create the linear regression model\n", - "\n", - "BigQuery DataFrames ML lets you move from exploring data to creating machine learning models through its scikit-learn-like API, `bigframes.ml`. BigQuery DataFrames ML supports several types of [ML models](https://cloud.google.com/python/docs/reference/bigframes/latest#ml-capabilities).\n", - "\n", - "In this notebook, you create a linear regression model, a type of regression model that generates a continuous value from a linear combination of input features.\n", - "\n", - "When you create a model with BigQuery DataFrames ML, it is saved locally and limited to the BigQuery session. However, as you'll see in the next section, you can use `to_gbq` to save the model permanently to your BigQuery project." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "EloGtMnverFF" - }, - "source": [ - "### Create the model using `bigframes.ml`\n", - "\n", - "When you pass the feature columns without transforms, BigQuery ML uses\n", - "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values.\n", - "\n", - "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "GskyyUQPowBT" - }, - "outputs": [], - "source": [ - "from bigframes.ml.linear_model import LinearRegression\n", - "\n", - "model = LinearRegression()\n", - "\n", - "model.fit(feature_columns, label_columns)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "UGjeMPC2caKK" - }, - "source": [ - "### Score the model\n", - "\n", - "Check how the model performed by using the `score` method. More information on model scoring can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "kGBJKafpo0dl" - }, - "outputs": [], - "source": [ - "model.score(feature_columns, label_columns)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "P2lUiZZ_cjri" - }, - "source": [ - "### Predict using the model\n", - "\n", - "Use the model to predict the body mass of the data row you saved earlier to the `test_data` DataFrame:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bsQ9cmoWo0Ps" - }, - "outputs": [], - "source": [ - "model.predict(test_data)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GTRdUw-Ro5R1" - }, - "source": [ - "## Save the model in BigQuery\n", - "\n", - "The model is saved locally within this session. You can save the model permanently to BigQuery for use in future sessions, and to make the model sharable with others." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "K0mPaoGpcwwy" - }, - "source": [ - "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ZSP7gt13QrQt" - }, - "outputs": [], - "source": [ - "DATASET_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "from google.cloud import bigquery\n", - "client = bigquery.Client(project=PROJECT_ID)\n", - "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", - "dataset.location = REGION\n", - "dataset = client.create_dataset(dataset, exists_ok=True)\n", - "print(f\"Dataset {dataset.dataset_id} created.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zqAIWWgJczp-" - }, - "source": [ - "Save the model using the `to_gbq` method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "QE_GD4Byo_jb" - }, - "outputs": [], - "source": [ - "model.to_gbq(DATASET_ID + \".penguin_weight\" , replace=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f7uHacAy49rT" - }, - "source": [ - "You can view the saved model in the BigQuery console under the dataset you created in the first step. Run the following cell and follow the link to view your BigQuery console:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "qDBoiA_0488Z" - }, - "outputs": [], - "source": [ - "print(f'https://console.developers.google.com/bigquery?p={PROJECT_ID}')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "G_wjSfXpWTuy" - }, - "source": [ - "# Summary and next steps\n", - "\n", - "You've created a linear regression model using `bigframes.ml`.\n", - "\n", - "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "# # Delete the BigQuery dataset and associated ML model\n", - "# from google.cloud import bigquery\n", - "# client = bigquery.Client(project=PROJECT_ID)\n", - "# client.delete_dataset(\n", - "# DATASET_ID, delete_contents=True, not_found_ok=True\n", - "# )\n", - "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" - ] - } - ], - "metadata": { - "colab": { - "provenance": [], - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "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.0" - } - }, - "nbformat": 4, - "nbformat_minor": 0 + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 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": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a linear regression model with BigQuery DataFrames ML", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to learn how to train a linear regression model using BigQuery DataFrames ML. BigQuery DataFrames ML provides a provides a scikit-learn-like API for ML powered by the BigQuery engine.\n", + "\n", + "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial).\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you use BigQuery DataFrames to create a linear regression model that predicts the weight of an Adelie penguin based on the penguin's island of residence, culmen length and depth, flipper length, and sex.\n", + "\n", + "The steps include:\n", + "\n", + "- Creating a DataFrame from a BigQuery table.\n", + "- Cleaning and preparing data using pandas.\n", + "- Creating a linear regression model using `bigframes.ml`.\n", + "- Saving the ML model to BigQuery for future use." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery Public Dataset) which includes data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", + "\n", + "1. Install the package\n", + "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9O0Ka4W2MNF3" + }, + "outputs": [], + "source": [ + "# !pip install bigframes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oDfTjfACBvJk" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D21CoOlfFTYI" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Read a BigQuery table into a BigQuery DataFrames DataFrame\n", + "\n", + "Read the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) into a BigQuery DataFrames DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], + "source": [ + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DJu837YEXD7B" + }, + "source": [ + "Take a look at the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_gPD0Zn1Stdb" + }, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Clean and prepare data\n", + "\n", + "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment.\n", + "\n", + "Because this model will focus on the Adelie Penguin species, you need to filter the data for only those rows representing Adelie penguins. Then you drop the `species` column because it is no longer needed.\n", + "\n", + "As these functions are applied, only the new DataFrame object `adelie_data` is modified. The source table and the original DataFrame object `df` don't change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [], + "source": [ + "# Filter down to the data to the Adelie Penguin species\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# Drop the species column\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# Take a look at the filtered DataFrame\n", + "adelie_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jhK2OlyMbY4L" + }, + "source": [ + "Drop rows with `NULL` values in order to create a BigQuery DataFrames DataFrame for the training data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0am3hdlXZfxZ" + }, + "outputs": [], + "source": [ + "# Drop rows with nulls to get training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# Take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M_-0X7NxYK5f" + }, + "source": [ + "Specify your feature (or input) columns and the label (or output) column:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YKwCW7Nsavap" + }, + "outputs": [], + "source": [ + "feature_columns = training_data[['island', 'culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 'sex']]\n", + "label_columns = training_data[['body_mass_g']]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CjyM7vZJZ0sQ" + }, + "source": [ + "There is a row within the `adelie_data` BigQuery DataFrames DataFrame that has a `NULL` value for the `body mass` column. `body mass` is the label column, which is the value that the model you are creating is trying to predict.\n", + "\n", + "Create a new BigQuery DataFrames DataFrame, `test_data`, for this row so that you can use it as test data on which to make a prediction later:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wej78IDUaRW9" + }, + "outputs": [], + "source": [ + "test_data = adelie_data[adelie_data.body_mass_g.isnull()]\n", + "\n", + "test_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, + "source": [ + "## Create the linear regression model\n", + "\n", + "BigQuery DataFrames ML lets you move from exploring data to creating machine learning models through its scikit-learn-like API, `bigframes.ml`. BigQuery DataFrames ML supports several types of [ML models](https://cloud.google.com/python/docs/reference/bigframes/latest#ml-capabilities).\n", + "\n", + "In this notebook, you create a linear regression model, a type of regression model that generates a continuous value from a linear combination of input features.\n", + "\n", + "When you create a model with BigQuery DataFrames ML, it is saved locally and limited to the BigQuery session. However, as you'll see in the next section, you can use `to_gbq` to save the model permanently to your BigQuery project." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EloGtMnverFF" + }, + "source": [ + "### Create the model using `bigframes.ml`\n", + "\n", + "When you pass the feature columns without transforms, BigQuery ML uses\n", + "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values.\n", + "\n", + "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "model = LinearRegression()\n", + "\n", + "model.fit(feature_columns, label_columns)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UGjeMPC2caKK" + }, + "source": [ + "### Score the model\n", + "\n", + "Check how the model performed by using the `score` method. More information on model scoring can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kGBJKafpo0dl" + }, + "outputs": [], + "source": [ + "model.score(feature_columns, label_columns)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P2lUiZZ_cjri" + }, + "source": [ + "### Predict using the model\n", + "\n", + "Use the model to predict the body mass of the data row you saved earlier to the `test_data` DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "outputs": [], + "source": [ + "model.predict(test_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, + "source": [ + "## Save the model in BigQuery\n", + "\n", + "The model is saved locally within this session. You can save the model permanently to BigQuery for use in future sessions, and to make the model sharable with others." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K0mPaoGpcwwy" + }, + "source": [ + "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZSP7gt13QrQt" + }, + "outputs": [], + "source": [ + "DATASET_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "from google.cloud import bigquery\n", + "client = bigquery.Client(project=PROJECT_ID)\n", + "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", + "dataset.location = REGION\n", + "dataset = client.create_dataset(dataset, exists_ok=True)\n", + "print(f\"Dataset {dataset.dataset_id} created.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zqAIWWgJczp-" + }, + "source": [ + "Save the model using the `to_gbq` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QE_GD4Byo_jb" + }, + "outputs": [], + "source": [ + "model.to_gbq(DATASET_ID + \".penguin_weight\" , replace=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f7uHacAy49rT" + }, + "source": [ + "You can view the saved model in the BigQuery console under the dataset you created in the first step. Run the following cell and follow the link to view your BigQuery console:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qDBoiA_0488Z" + }, + "outputs": [], + "source": [ + "print(f'https://console.developers.google.com/bigquery?p={PROJECT_ID}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G_wjSfXpWTuy" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "You've created a linear regression model using `bigframes.ml`.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery dataset and associated ML model\n", + "# from google.cloud import bigquery\n", + "# client = bigquery.Client(project=PROJECT_ID)\n", + "# client.delete_dataset(\n", + "# DATASET_ID, delete_contents=True, not_found_ok=True\n", + "# )\n", + "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/ml/bq_dataframes_ml_linear_regression_bbq.ipynb b/notebooks/ml/bq_dataframes_ml_linear_regression_bbq.ipynb index 6be836c6f81..396fde5a397 100644 --- a/notebooks/ml/bq_dataframes_ml_linear_regression_bbq.ipynb +++ b/notebooks/ml/bq_dataframes_ml_linear_regression_bbq.ipynb @@ -1,2637 +1,2637 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "outputs": [], - "source": [ - "# Copyright 2023 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": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "## Train a linear regression model with BigQuery DataFrames ML\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "24743cf4a1e1" - }, - "source": [ - "**_NOTE_**: This notebook has been tested in the following environment:\n", - "\n", - "* Python version = 3.10" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "Use this notebook to learn how to train a linear regression model using BigQuery ML and the `bigframes.bigquery` module.\n", - "\n", - "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial).\n", - "\n", - "Learn more about [BigQuery DataFrames](https://dataframes.bigquery.dev/)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, you use BigQuery DataFrames to create a linear regression model that predicts the weight of an Adelie penguin based on the penguin's island of residence, culmen length and depth, flipper length, and sex.\n", - "\n", - "The steps include:\n", - "\n", - "- Creating a DataFrame from a BigQuery table.\n", - "- Cleaning and preparing data using pandas.\n", - "- Creating a linear regression model using `bigframes.ml`.\n", - "- Saving the ML model to BigQuery for future use." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery Public Dataset) which includes data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "* BigQuery ML\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", - "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", - "\n", - "1. Install the package\n", - "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "9O0Ka4W2MNF3" - }, - "outputs": [], - "source": [ - "# !pip install bigframes" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "# import IPython\n", - "\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] - }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2023 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": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a linear regression model with BigQuery DataFrames ML", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Use this notebook to learn how to train a linear regression model using BigQuery ML and the `bigframes.bigquery` module.\n", + "\n", + "This example is adapted from the [BQML linear regression tutorial](https://cloud.google.com/bigquery-ml/docs/linear-regression-tutorial).\n", + "\n", + "Learn more about [BigQuery DataFrames](https://dataframes.bigquery.dev/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you use BigQuery DataFrames to create a linear regression model that predicts the weight of an Adelie penguin based on the penguin's island of residence, culmen length and depth, flipper length, and sex.\n", + "\n", + "The steps include:\n", + "\n", + "- Creating a DataFrame from a BigQuery table.\n", + "- Cleaning and preparing data using pandas.\n", + "- Creating a linear regression model using `bigframes.ml`.\n", + "- Saving the ML model to BigQuery for future use." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This tutorial uses the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) (a BigQuery Public Dataset) which includes data on a set of penguins including species, island of residence, weight, culmen length and depth, flipper length, and sex." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", + "\n", + "1. Install the package\n", + "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "9O0Ka4W2MNF3" + }, + "outputs": [], + "source": [ + "# !pip install bigframes" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oDfTjfACBvJk" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "oDfTjfACBvJk" - }, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n" + ] + } + ], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION\n", + "\n", + "# Recommended for performance. Disables pandas default ordering of all rows.\n", + "bpd.options.bigquery.ordering_mode = \"partial\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D21CoOlfFTYI" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Read a BigQuery table into a BigQuery DataFrames DataFrame\n", + "\n", + "Read the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) into a BigQuery DataFrames DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], + "source": [ + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DJu837YEXD7B" + }, + "source": [ + "Take a look at the DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "_gPD0Zn1Stdb" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "If you don't know your project ID, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [ + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "index", + "rawType": "int64", + "type": "integer" + }, + { + "name": "species", + "rawType": "string", + "type": "string" + }, + { + "name": "island", + "rawType": "string", + "type": "string" + }, + { + "name": "culmen_length_mm", + "rawType": "Float64", + "type": "float" + }, + { + "name": "culmen_depth_mm", + "rawType": "Float64", + "type": "float" + }, + { + "name": "flipper_length_mm", + "rawType": "Float64", + "type": "float" + }, + { + "name": "body_mass_g", + "rawType": "Float64", + "type": "float" + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Updated property [core/project].\n" - ] + "name": "sex", + "rawType": "string", + "type": "string" } - ], - "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "# Set the project id\n", - "! gcloud config set project {PROJECT_ID}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" + ], + "ref": "a652ba52-0445-4228-a2d5-baf837933515", + "rows": [ + [ + "0", + "Adelie Penguin (Pygoscelis adeliae)", + "Dream", + "36.6", + "18.4", + "184.0", + "3475.0", + "FEMALE" + ], + [ + "1", + "Adelie Penguin (Pygoscelis adeliae)", + "Dream", + "39.8", + "19.1", + "184.0", + "4650.0", + "MALE" + ], + [ + "2", + "Adelie Penguin (Pygoscelis adeliae)", + "Dream", + "40.9", + "18.9", + "184.0", + "3900.0", + "MALE" + ], + [ + "3", + "Chinstrap penguin (Pygoscelis antarctica)", + "Dream", + "46.5", + "17.9", + "192.0", + "3500.0", + "FEMALE" + ], + [ + "4", + "Adelie Penguin (Pygoscelis adeliae)", + "Dream", + "37.3", + "16.8", + "192.0", + "3000.0", + "FEMALE" + ] + ], + "shape": { + "columns": 7, + "rows": 5 + } }, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "eF-Twtc4XGem" - }, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" + "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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
\n", + "
" + ], + "text/plain": [ + " 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", + "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": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Clean and prepare data\n", + "\n", + "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment.\n", + "\n", + "Because this model will focus on the Adelie Penguin species, you need to filter the data for only those rows representing Adelie penguins. Then you drop the `species` column because it is no longer needed.\n", + "\n", + "As these functions are applied, only the new DataFrame object `adelie_data` is modified. The source table and the original DataFrame object `df` don't change." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Authenticate your Google Cloud account\n", - "\n", - "Depending on your Jupyter environment, you might have to manually authenticate. Follow the relevant instructions below." + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 28.9 kB in 12 seconds of slot time. [Job bigframes-dev:US.bb256e8c-f2c7-4eff-b5f3-fcc6836110cf details]\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 8.4 kB in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login" + "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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
3Dream37.316.8192.03000.0FEMALE
4Dream43.218.5192.04100.0MALE
5Dream40.220.1200.03975.0MALE
6Dream40.818.9208.04300.0MALE
7Dream39.018.7185.03650.0MALE
8Dream37.016.9185.03000.0FEMALE
9Dream34.017.1185.03400.0FEMALE
\n", + "

10 rows × 6 columns

\n", + "
[152 rows x 6 columns in total]" + ], + "text/plain": [ + "island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + " Dream 36.6 18.4 184.0 3475.0 \n", + " Dream 39.8 19.1 184.0 4650.0 \n", + " Dream 40.9 18.9 184.0 3900.0 \n", + " Dream 37.3 16.8 192.0 3000.0 \n", + " Dream 43.2 18.5 192.0 4100.0 \n", + " Dream 40.2 20.1 200.0 3975.0 \n", + " Dream 40.8 18.9 208.0 4300.0 \n", + " Dream 39.0 18.7 185.0 3650.0 \n", + " Dream 37.0 16.9 185.0 3000.0 \n", + " Dream 34.0 17.1 185.0 3400.0 \n", + "\n", + " sex \n", + "FEMALE \n", + " MALE \n", + " MALE \n", + "FEMALE \n", + " MALE \n", + " MALE \n", + " MALE \n", + " MALE \n", + "FEMALE \n", + "FEMALE \n", + "...\n", + "\n", + "[152 rows x 6 columns]" ] - }, + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Filter down to the data to the Adelie Penguin species\n", + "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", + "\n", + "# Drop the species column\n", + "adelie_data = adelie_data.drop(columns=[\"species\"])\n", + "\n", + "# Take a look at the filtered DataFrame\n", + "adelie_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jhK2OlyMbY4L" + }, + "source": [ + "Drop rows with `NULL` values in order to create a BigQuery DataFrames DataFrame for the training data:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "0am3hdlXZfxZ" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" + "data": { + "text/html": [ + "Starting." + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 8.1 kB in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries" + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bpd" + "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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
3Dream37.316.8192.03000.0FEMALE
4Dream43.218.5192.04100.0MALE
5Dream40.220.1200.03975.0MALE
6Dream40.818.9208.04300.0MALE
7Dream39.018.7185.03650.0MALE
8Dream37.016.9185.03000.0FEMALE
9Dream34.017.1185.03400.0FEMALE
\n", + "

10 rows × 6 columns

\n", + "
[146 rows x 6 columns in total]" + ], + "text/plain": [ + "island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", + " Dream 36.6 18.4 184.0 3475.0 \n", + " Dream 39.8 19.1 184.0 4650.0 \n", + " Dream 40.9 18.9 184.0 3900.0 \n", + " Dream 37.3 16.8 192.0 3000.0 \n", + " Dream 43.2 18.5 192.0 4100.0 \n", + " Dream 40.2 20.1 200.0 3975.0 \n", + " Dream 40.8 18.9 208.0 4300.0 \n", + " Dream 39.0 18.7 185.0 3650.0 \n", + " Dream 37.0 16.9 185.0 3000.0 \n", + " Dream 34.0 17.1 185.0 3400.0 \n", + "\n", + " sex \n", + "FEMALE \n", + " MALE \n", + " MALE \n", + "FEMALE \n", + " MALE \n", + " MALE \n", + " MALE \n", + " MALE \n", + "FEMALE \n", + "FEMALE \n", + "...\n", + "\n", + "[146 rows x 6 columns]" ] - }, + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Drop rows with nulls to get training data\n", + "training_data = adelie_data.dropna()\n", + "\n", + "# Take a peek at the training data\n", + "training_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, + "source": [ + "## Create the linear regression model\n", + "\n", + "In this notebook, you create a linear regression model, a type of regression model that generates a continuous value from a linear combination of input features." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "### Set BigQuery DataFrames options" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset bqml_tutorial created.\n" + ] + } + ], + "source": [ + "DATASET_ID = \"bqml_tutorial\" # @param {type:\"string\"}\n", + "\n", + "from google.cloud import bigquery\n", + "client = bigquery.Client(project=PROJECT_ID)\n", + "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", + "dataset.location = REGION\n", + "dataset = client.create_dataset(dataset, exists_ok=True)\n", + "print(f\"Dataset {dataset.dataset_id} created.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EloGtMnverFF" + }, + "source": [ + "### Create the model using `bigframes.bigquery.ml.create_model`\n", + "\n", + "When you pass the feature columns without transforms, BigQuery ML uses\n", + "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values.\n", + "\n", + "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "NPPMuw2PXGeo" - }, - "outputs": [], - "source": [ - "# Note: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bpd.options.bigquery.project = PROJECT_ID\n", - "\n", - "# Note: The location option is not required.\n", - "# It defaults to the location of the first table or query\n", - "# passed to read_gbq(). For APIs where a location can't be\n", - "# auto-detected, the location defaults to the \"US\" location.\n", - "bpd.options.bigquery.location = REGION\n", - "\n", - "# Recommended for performance. Disables pandas default ordering of all rows.\n", - "bpd.options.bigquery.ordering_mode = \"partial\"" + "data": { + "text/html": [ + "\n", + " Query started with request ID bigframes-dev:US.a33b3628-730b-46e8-ad17-c78bb48619ce.
SQL
CREATE OR REPLACE MODEL `bigframes-dev.bqml_tutorial.penguin_weight`\n",
+       "OPTIONS(model_type = 'LINEAR_REG')\n",
+       "AS SELECT\n",
+       "`bfuid_col_3` AS `island`,\n",
+       "`bfuid_col_4` AS `culmen_length_mm`,\n",
+       "`bfuid_col_5` AS `culmen_depth_mm`,\n",
+       "`bfuid_col_6` AS `flipper_length_mm`,\n",
+       "`bfuid_col_7` AS `label`,\n",
+       "`bfuid_col_8` AS `sex`\n",
+       "FROM\n",
+       "(SELECT\n",
+       "  `t0`.`bfuid_col_3`,\n",
+       "  `t0`.`bfuid_col_4`,\n",
+       "  `t0`.`bfuid_col_5`,\n",
+       "  `t0`.`bfuid_col_6`,\n",
+       "  `t0`.`bfuid_col_7`,\n",
+       "  `t0`.`bfuid_col_8`\n",
+       "FROM `bigframes-dev._63cfa399614a54153cc386c27d6c0c6fdb249f9e._e154f0aa_5b29_492a_b464_a77c5f5a3dbd_bqdf_60fa3196-5a3e-45ae-898e-c2b473bfa1e9` AS `t0`)\n",
+       "
\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "D21CoOlfFTYI" + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "index", + "rawType": "object", + "type": "string" + }, + { + "name": "0", + "rawType": "object", + "type": "unknown" + } + ], + "ref": "851c170c-08a5-4c06-8c0b-4547dbde3f18", + "rows": [ + [ + "etag", + "P3XS+g0ZZM19ywL+hdwUmQ==" + ], + [ + "modelReference", + "{'projectId': 'bigframes-dev', 'datasetId': 'bqml_tutorial', 'modelId': 'penguin_weight'}" + ], + [ + "creationTime", + "1764779445166" + ], + [ + "lastModifiedTime", + "1764779445237" + ], + [ + "modelType", + "LINEAR_REGRESSION" + ], + [ + "trainingRuns", + "[{'trainingOptions': {'lossType': 'MEAN_SQUARED_LOSS', 'l2Regularization': 0, 'inputLabelColumns': ['label'], 'dataSplitMethod': 'AUTO_SPLIT', 'optimizationStrategy': 'NORMAL_EQUATION', 'calculatePValues': False, 'enableGlobalExplain': False, 'categoryEncodingMethod': 'ONE_HOT_ENCODING', 'fitIntercept': True, 'standardizeFeatures': True}, 'trainingStartTime': '1764779429690', 'results': [{'index': 0, 'durationMs': '3104', 'trainingLoss': 78553.60163372214}], 'evaluationMetrics': {'regressionMetrics': {'meanAbsoluteError': 223.87876300779865, 'meanSquaredError': 78553.60163372215, 'meanSquaredLogError': 0.005614202871872688, 'medianAbsoluteError': 181.33091105963013, 'rSquared': 0.6239507555914934}}, 'startTime': '2025-12-03T16:30:29.690Z'}]" + ], + [ + "featureColumns", + "[{'name': 'island', 'type': {'typeKind': 'STRING'}}, {'name': 'culmen_length_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'culmen_depth_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'flipper_length_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'sex', 'type': {'typeKind': 'STRING'}}]" + ], + [ + "labelColumns", + "[{'name': 'predicted_label', 'type': {'typeKind': 'FLOAT64'}}]" + ], + [ + "location", + "US" + ] + ], + "shape": { + "columns": 1, + "rows": 9 + } }, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." + "text/plain": [ + "etag P3XS+g0ZZM19ywL+hdwUmQ==\n", + "modelReference {'projectId': 'bigframes-dev', 'datasetId': 'b...\n", + "creationTime 1764779445166\n", + "lastModifiedTime 1764779445237\n", + "modelType LINEAR_REGRESSION\n", + "trainingRuns [{'trainingOptions': {'lossType': 'MEAN_SQUARE...\n", + "featureColumns [{'name': 'island', 'type': {'typeKind': 'STRI...\n", + "labelColumns [{'name': 'predicted_label', 'type': {'typeKin...\n", + "location US\n", + "dtype: object" ] - }, + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import bigframes.bigquery as bbq\n", + "\n", + "model_name = f\"{PROJECT_ID}.{DATASET_ID}.penguin_weight\"\n", + "model_metadata = bbq.ml.create_model(\n", + " model_name,\n", + " replace=True,\n", + " options={\n", + " \"model_type\": \"LINEAR_REG\",\n", + " },\n", + " training_data=training_data.rename(columns={\"body_mass_g\": \"label\"})\n", + ")\n", + "model_metadata" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GskyyUQPowBT" + }, + "source": [ + "### Evaluate the model\n", + "\n", + "Check how the model performed by using the `evalutate` function. More information on model evaluation can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "kGBJKafpo0dl" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "9EMAqR37AfLS" - }, - "source": [ - "## Read a BigQuery table into a BigQuery DataFrames DataFrame\n", - "\n", - "Read the [```penguins``` table](https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=ml_datasets&t=penguins) into a BigQuery DataFrames DataFrame:" + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 0 Bytes in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "EDAaIwHpQCDZ" - }, - "outputs": [], - "source": [ - "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")" + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "DJu837YEXD7B" - }, - "source": [ - "Take a look at the DataFrame:" + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "_gPD0Zn1Stdb" - }, - "outputs": [ - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "int64", - "type": "integer" - }, - { - "name": "species", - "rawType": "string", - "type": "string" - }, - { - "name": "island", - "rawType": "string", - "type": "string" - }, - { - "name": "culmen_length_mm", - "rawType": "Float64", - "type": "float" - }, - { - "name": "culmen_depth_mm", - "rawType": "Float64", - "type": "float" - }, - { - "name": "flipper_length_mm", - "rawType": "Float64", - "type": "float" - }, - { - "name": "body_mass_g", - "rawType": "Float64", - "type": "float" - }, - { - "name": "sex", - "rawType": "string", - "type": "string" - } - ], - "ref": "a652ba52-0445-4228-a2d5-baf837933515", - "rows": [ - [ - "0", - "Adelie Penguin (Pygoscelis adeliae)", - "Dream", - "36.6", - "18.4", - "184.0", - "3475.0", - "FEMALE" - ], - [ - "1", - "Adelie Penguin (Pygoscelis adeliae)", - "Dream", - "39.8", - "19.1", - "184.0", - "4650.0", - "MALE" - ], - [ - "2", - "Adelie Penguin (Pygoscelis adeliae)", - "Dream", - "40.9", - "18.9", - "184.0", - "3900.0", - "MALE" - ], - [ - "3", - "Chinstrap penguin (Pygoscelis antarctica)", - "Dream", - "46.5", - "17.9", - "192.0", - "3500.0", - "FEMALE" - ], - [ - "4", - "Adelie Penguin (Pygoscelis adeliae)", - "Dream", - "37.3", - "16.8", - "192.0", - "3000.0", - "FEMALE" - ] - ], - "shape": { - "columns": 7, - "rows": 5 - } - }, - "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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Adelie Penguin (Pygoscelis adeliae)Dream36.618.4184.03475.0FEMALE
1Adelie Penguin (Pygoscelis adeliae)Dream39.819.1184.04650.0MALE
2Adelie Penguin (Pygoscelis adeliae)Dream40.918.9184.03900.0MALE
3Chinstrap penguin (Pygoscelis antarctica)Dream46.517.9192.03500.0FEMALE
4Adelie Penguin (Pygoscelis adeliae)Dream37.316.8192.03000.0FEMALE
\n", - "
" - ], - "text/plain": [ - " 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", - "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": 11, - "metadata": {}, - "output_type": "execute_result" - } + "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", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "

1 rows × 6 columns

\n", + "
[1 rows x 6 columns in total]" ], - "source": [ - "df.peek()" + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + "0 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + "0 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" ] - }, + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.ml.evaluate(model_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P2lUiZZ_cjri" + }, + "source": [ + "### Use the model to predict outcomes\n", + "\n", + "Now that you have evaluated your model, the next step is to use it to predict an\n", + "outcome. You can run `bigframes.bigquery.ml.predict` function on the model to\n", + "predict the body mass in grams of all penguins that reside on the Biscoe\n", + "Islands." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "rwPLjqW2Ajzh" - }, - "source": [ - "## Clean and prepare data\n", - "\n", - "You can use pandas as you normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of your local environment.\n", - "\n", - "Because this model will focus on the Adelie Penguin species, you need to filter the data for only those rows representing Adelie penguins. Then you drop the `species` column because it is no longer needed.\n", - "\n", - "As these functions are applied, only the new DataFrame object `adelie_data` is modified. The source table and the original DataFrame object `df` don't change." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/log_adapter.py:182: TimeTravelCacheWarning: Reading cached table from 2025-12-03 16:30:18.272882+00:00 to avoid\n", + "incompatibilies with previous reads of this table. To read the latest\n", + "version, set `use_cache=False` or close the current session with\n", + "Session.close() or bigframes.pandas.close_session().\n", + " return method(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "id": "6i6HkFJZa8na" - }, - "outputs": [ - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 28.9 kB in 12 seconds of slot time. [Job bigframes-dev:US.bb256e8c-f2c7-4eff-b5f3-fcc6836110cf details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 8.4 kB in a moment of slot time.\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
3Dream37.316.8192.03000.0FEMALE
4Dream43.218.5192.04100.0MALE
5Dream40.220.1200.03975.0MALE
6Dream40.818.9208.04300.0MALE
7Dream39.018.7185.03650.0MALE
8Dream37.016.9185.03000.0FEMALE
9Dream34.017.1185.03400.0FEMALE
\n", - "

10 rows × 6 columns

\n", - "
[152 rows x 6 columns in total]" - ], - "text/plain": [ - "island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - " Dream 36.6 18.4 184.0 3475.0 \n", - " Dream 39.8 19.1 184.0 4650.0 \n", - " Dream 40.9 18.9 184.0 3900.0 \n", - " Dream 37.3 16.8 192.0 3000.0 \n", - " Dream 43.2 18.5 192.0 4100.0 \n", - " Dream 40.2 20.1 200.0 3975.0 \n", - " Dream 40.8 18.9 208.0 4300.0 \n", - " Dream 39.0 18.7 185.0 3650.0 \n", - " Dream 37.0 16.9 185.0 3000.0 \n", - " Dream 34.0 17.1 185.0 3400.0 \n", - "\n", - " sex \n", - "FEMALE \n", - " MALE \n", - " MALE \n", - "FEMALE \n", - " MALE \n", - " MALE \n", - " MALE \n", - " MALE \n", - "FEMALE \n", - "FEMALE \n", - "...\n", - "\n", - "[152 rows x 6 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 29.3 kB in a moment of slot time.\n", + " " ], - "source": [ - "# Filter down to the data to the Adelie Penguin species\n", - "adelie_data = df[df.species == \"Adelie Penguin (Pygoscelis adeliae)\"]\n", - "\n", - "# Drop the species column\n", - "adelie_data = adelie_data.drop(columns=[\"species\"])\n", - "\n", - "# Take a look at the filtered DataFrame\n", - "adelie_data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "jhK2OlyMbY4L" - }, - "source": [ - "Drop rows with `NULL` values in order to create a BigQuery DataFrames DataFrame for the training data:" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "id": "0am3hdlXZfxZ" - }, - "outputs": [ - { - "data": { - "text/html": [ - "Starting." - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 8.1 kB in a moment of slot time.\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
islandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
0Dream36.618.4184.03475.0FEMALE
1Dream39.819.1184.04650.0MALE
2Dream40.918.9184.03900.0MALE
3Dream37.316.8192.03000.0FEMALE
4Dream43.218.5192.04100.0MALE
5Dream40.220.1200.03975.0MALE
6Dream40.818.9208.04300.0MALE
7Dream39.018.7185.03650.0MALE
8Dream37.016.9185.03000.0FEMALE
9Dream34.017.1185.03400.0FEMALE
\n", - "

10 rows × 6 columns

\n", - "
[146 rows x 6 columns in total]" - ], - "text/plain": [ - "island culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g \\\n", - " Dream 36.6 18.4 184.0 3475.0 \n", - " Dream 39.8 19.1 184.0 4650.0 \n", - " Dream 40.9 18.9 184.0 3900.0 \n", - " Dream 37.3 16.8 192.0 3000.0 \n", - " Dream 43.2 18.5 192.0 4100.0 \n", - " Dream 40.2 20.1 200.0 3975.0 \n", - " Dream 40.8 18.9 208.0 4300.0 \n", - " Dream 39.0 18.7 185.0 3650.0 \n", - " Dream 37.0 16.9 185.0 3000.0 \n", - " Dream 34.0 17.1 185.0 3400.0 \n", - "\n", - " sex \n", - "FEMALE \n", - " MALE \n", - " MALE \n", - "FEMALE \n", - " MALE \n", - " MALE \n", - " MALE \n", - " MALE \n", - "FEMALE \n", - "FEMALE \n", - "...\n", - "\n", - "[146 rows x 6 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. " ], - "source": [ - "# Drop rows with nulls to get training data\n", - "training_data = adelie_data.dropna()\n", - "\n", - "# Take a peek at the training data\n", - "training_data" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "Fx4lsNqMorJ-" - }, - "source": [ - "## Create the linear regression model\n", - "\n", - "In this notebook, you create a linear regression model, a type of regression model that generates a continuous value from a linear combination of input features." + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" + "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", + " \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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + "
predicted_labelspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
03945.010052Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
13914.916297Adelie Penguin (Pygoscelis adeliae)Biscoe39.718.9184.03550.0MALE
23278.611224Adelie Penguin (Pygoscelis adeliae)Biscoe36.417.1184.02850.0FEMALE
34006.367355Adelie Penguin (Pygoscelis adeliae)Biscoe41.618.0192.03950.0MALE
43417.610478Adelie Penguin (Pygoscelis adeliae)Biscoe35.017.9192.03725.0FEMALE
54009.612421Adelie Penguin (Pygoscelis adeliae)Biscoe41.118.2192.04050.0MALE
64231.330911Adelie Penguin (Pygoscelis adeliae)Biscoe42.019.5200.04050.0MALE
73554.308906Gentoo penguin (Pygoscelis papua)Biscoe43.813.9208.04300.0FEMALE
83550.677455Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
93537.882543Gentoo penguin (Pygoscelis papua)Biscoe44.013.6208.04350.0FEMALE
\n", + "

10 rows × 8 columns

\n", + "
[168 rows x 8 columns in total]" + ], + "text/plain": [ + " predicted_label species island \\\n", + "0 3945.010052 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1 3914.916297 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "2 3278.611224 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "3 4006.367355 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "4 3417.610478 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "5 4009.612421 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "6 4231.330911 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", + "7 3554.308906 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "8 3550.677455 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "9 3537.882543 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "\n", + " culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 \n", + "1 39.7 18.9 184.0 3550.0 MALE \n", + "2 36.4 17.1 184.0 2850.0 FEMALE \n", + "3 41.6 18.0 192.0 3950.0 MALE \n", + "4 35.0 17.9 192.0 3725.0 FEMALE \n", + "5 41.1 18.2 192.0 4050.0 MALE \n", + "6 42.0 19.5 200.0 4050.0 MALE \n", + "7 43.8 13.9 208.0 4300.0 FEMALE \n", + "8 43.3 14.0 208.0 4575.0 FEMALE \n", + "9 44.0 13.6 208.0 4350.0 FEMALE \n", + "...\n", + "\n", + "[168 rows x 8 columns]" ] - }, + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", + "biscoe = df[df[\"island\"].str.contains(\"Biscoe\")]\n", + "bbq.ml.predict(model_name, biscoe)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, + "source": [ + "### Explain the prediction results\n", + "\n", + "To understand why the model is generating these prediction results, you can use the `explain_predict` function." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dataset bqml_tutorial created.\n" - ] - } + "data": { + "text/html": [ + "\n", + " Query started with request ID bigframes-dev:US.161bba69-c852-4916-a2df-bb5b309be6e4.
SQL
SELECT * FROM ML.EXPLAIN_PREDICT(MODEL `bigframes-dev.bqml_tutorial.penguin_weight`, (SELECT\n",
+       "`bfuid_col_22` AS `species`,\n",
+       "`bfuid_col_23` AS `island`,\n",
+       "`bfuid_col_24` AS `culmen_length_mm`,\n",
+       "`bfuid_col_25` AS `culmen_depth_mm`,\n",
+       "`bfuid_col_26` AS `flipper_length_mm`,\n",
+       "`bfuid_col_27` AS `body_mass_g`,\n",
+       "`bfuid_col_28` AS `sex`\n",
+       "FROM\n",
+       "(SELECT\n",
+       "  `t0`.`species`,\n",
+       "  `t0`.`island`,\n",
+       "  `t0`.`culmen_length_mm`,\n",
+       "  `t0`.`culmen_depth_mm`,\n",
+       "  `t0`.`flipper_length_mm`,\n",
+       "  `t0`.`body_mass_g`,\n",
+       "  `t0`.`sex`,\n",
+       "  `t0`.`species` AS `bfuid_col_22`,\n",
+       "  `t0`.`island` AS `bfuid_col_23`,\n",
+       "  `t0`.`culmen_length_mm` AS `bfuid_col_24`,\n",
+       "  `t0`.`culmen_depth_mm` AS `bfuid_col_25`,\n",
+       "  `t0`.`flipper_length_mm` AS `bfuid_col_26`,\n",
+       "  `t0`.`body_mass_g` AS `bfuid_col_27`,\n",
+       "  `t0`.`sex` AS `bfuid_col_28`,\n",
+       "  regexp_contains(`t0`.`island`, 'Biscoe') AS `bfuid_col_29`\n",
+       "FROM (\n",
+       "  SELECT\n",
+       "    `species`,\n",
+       "    `island`,\n",
+       "    `culmen_length_mm`,\n",
+       "    `culmen_depth_mm`,\n",
+       "    `flipper_length_mm`,\n",
+       "    `body_mass_g`,\n",
+       "    `sex`\n",
+       "  FROM `bigquery-public-data.ml_datasets.penguins` FOR SYSTEM_TIME AS OF TIMESTAMP('2025-12-03T16:30:18.272882+00:00')\n",
+       ") AS `t0`\n",
+       "WHERE\n",
+       "  regexp_contains(`t0`.`island`, 'Biscoe'))), STRUCT(3 AS top_k_features))\n",
+       "
\n", + " " ], - "source": [ - "DATASET_ID = \"bqml_tutorial\" # @param {type:\"string\"}\n", - "\n", - "from google.cloud import bigquery\n", - "client = bigquery.Client(project=PROJECT_ID)\n", - "dataset = bigquery.Dataset(PROJECT_ID + \".\" + DATASET_ID)\n", - "dataset.location = REGION\n", - "dataset = client.create_dataset(dataset, exists_ok=True)\n", - "print(f\"Dataset {dataset.dataset_id} created.\")" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "EloGtMnverFF" - }, - "source": [ - "### Create the model using `bigframes.bigquery.ml.create_model`\n", - "\n", - "When you pass the feature columns without transforms, BigQuery ML uses\n", - "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values.\n", - "\n", - "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "id": "GskyyUQPowBT" - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " Query started with request ID bigframes-dev:US.a33b3628-730b-46e8-ad17-c78bb48619ce.
SQL
CREATE OR REPLACE MODEL `bigframes-dev.bqml_tutorial.penguin_weight`\n",
-              "OPTIONS(model_type = 'LINEAR_REG')\n",
-              "AS SELECT\n",
-              "`bfuid_col_3` AS `island`,\n",
-              "`bfuid_col_4` AS `culmen_length_mm`,\n",
-              "`bfuid_col_5` AS `culmen_depth_mm`,\n",
-              "`bfuid_col_6` AS `flipper_length_mm`,\n",
-              "`bfuid_col_7` AS `label`,\n",
-              "`bfuid_col_8` AS `sex`\n",
-              "FROM\n",
-              "(SELECT\n",
-              "  `t0`.`bfuid_col_3`,\n",
-              "  `t0`.`bfuid_col_4`,\n",
-              "  `t0`.`bfuid_col_5`,\n",
-              "  `t0`.`bfuid_col_6`,\n",
-              "  `t0`.`bfuid_col_7`,\n",
-              "  `t0`.`bfuid_col_8`\n",
-              "FROM `bigframes-dev._63cfa399614a54153cc386c27d6c0c6fdb249f9e._e154f0aa_5b29_492a_b464_a77c5f5a3dbd_bqdf_60fa3196-5a3e-45ae-898e-c2b473bfa1e9` AS `t0`)\n",
-              "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "object", - "type": "string" - }, - { - "name": "0", - "rawType": "object", - "type": "unknown" - } - ], - "ref": "851c170c-08a5-4c06-8c0b-4547dbde3f18", - "rows": [ - [ - "etag", - "P3XS+g0ZZM19ywL+hdwUmQ==" - ], - [ - "modelReference", - "{'projectId': 'bigframes-dev', 'datasetId': 'bqml_tutorial', 'modelId': 'penguin_weight'}" - ], - [ - "creationTime", - "1764779445166" - ], - [ - "lastModifiedTime", - "1764779445237" - ], - [ - "modelType", - "LINEAR_REGRESSION" - ], - [ - "trainingRuns", - "[{'trainingOptions': {'lossType': 'MEAN_SQUARED_LOSS', 'l2Regularization': 0, 'inputLabelColumns': ['label'], 'dataSplitMethod': 'AUTO_SPLIT', 'optimizationStrategy': 'NORMAL_EQUATION', 'calculatePValues': False, 'enableGlobalExplain': False, 'categoryEncodingMethod': 'ONE_HOT_ENCODING', 'fitIntercept': True, 'standardizeFeatures': True}, 'trainingStartTime': '1764779429690', 'results': [{'index': 0, 'durationMs': '3104', 'trainingLoss': 78553.60163372214}], 'evaluationMetrics': {'regressionMetrics': {'meanAbsoluteError': 223.87876300779865, 'meanSquaredError': 78553.60163372215, 'meanSquaredLogError': 0.005614202871872688, 'medianAbsoluteError': 181.33091105963013, 'rSquared': 0.6239507555914934}}, 'startTime': '2025-12-03T16:30:29.690Z'}]" - ], - [ - "featureColumns", - "[{'name': 'island', 'type': {'typeKind': 'STRING'}}, {'name': 'culmen_length_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'culmen_depth_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'flipper_length_mm', 'type': {'typeKind': 'FLOAT64'}}, {'name': 'sex', 'type': {'typeKind': 'STRING'}}]" - ], - [ - "labelColumns", - "[{'name': 'predicted_label', 'type': {'typeKind': 'FLOAT64'}}]" - ], - [ - "location", - "US" - ] - ], - "shape": { - "columns": 1, - "rows": 9 - } - }, - "text/plain": [ - "etag P3XS+g0ZZM19ywL+hdwUmQ==\n", - "modelReference {'projectId': 'bigframes-dev', 'datasetId': 'b...\n", - "creationTime 1764779445166\n", - "lastModifiedTime 1764779445237\n", - "modelType LINEAR_REGRESSION\n", - "trainingRuns [{'trainingOptions': {'lossType': 'MEAN_SQUARE...\n", - "featureColumns [{'name': 'island', 'type': {'typeKind': 'STRI...\n", - "labelColumns [{'name': 'predicted_label', 'type': {'typeKin...\n", - "location US\n", - "dtype: object" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. " ], - "source": [ - "import bigframes.bigquery as bbq\n", - "\n", - "model_name = f\"{PROJECT_ID}.{DATASET_ID}.penguin_weight\"\n", - "model_metadata = bbq.ml.create_model(\n", - " model_name,\n", - " replace=True,\n", - " options={\n", - " \"model_type\": \"LINEAR_REG\",\n", - " },\n", - " training_data=training_data.rename(columns={\"body_mass_g\": \"label\"})\n", - ")\n", - "model_metadata" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "GskyyUQPowBT" - }, - "source": [ - "### Evaluate the model\n", - "\n", - "Check how the model performed by using the `evalutate` function. More information on model evaluation can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." + "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", + " \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", + " \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", + " \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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_labeltop_feature_attributionsbaseline_prediction_valueprediction_valueapproximation_errorspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
03945.010052[{'feature': 'island', 'attribution': 0.0}\n", + " {'...3945.0100523945.0100520.0Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
13914.916297[{'feature': 'flipper_length_mm', 'attribution...3945.0100523914.9162970.0Adelie Penguin (Pygoscelis adeliae)Biscoe39.718.9184.03550.0MALE
23278.611224[{'feature': 'sex', 'attribution': -443.175184...3945.0100523278.6112240.0Adelie Penguin (Pygoscelis adeliae)Biscoe36.417.1184.02850.0FEMALE
34006.367355[{'feature': 'culmen_length_mm', 'attribution'...3945.0100524006.3673550.0Adelie Penguin (Pygoscelis adeliae)Biscoe41.618.0192.03950.0MALE
43417.610478[{'feature': 'sex', 'attribution': -443.175184...3945.0100523417.6104780.0Adelie Penguin (Pygoscelis adeliae)Biscoe35.017.9192.03725.0FEMALE
54009.612421[{'feature': 'culmen_length_mm', 'attribution'...3945.0100524009.6124210.0Adelie Penguin (Pygoscelis adeliae)Biscoe41.118.2192.04050.0MALE
64231.330911[{'feature': 'flipper_length_mm', 'attribution...3945.0100524231.3309110.0Adelie Penguin (Pygoscelis adeliae)Biscoe42.019.5200.04050.0MALE
73554.308906[{'feature': 'sex', 'attribution': -443.175184...3945.0100523554.3089060.0Gentoo penguin (Pygoscelis papua)Biscoe43.813.9208.04300.0FEMALE
83550.677455[{'feature': 'sex', 'attribution': -443.175184...3945.0100523550.6774550.0Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
93537.882543[{'feature': 'sex', 'attribution': -443.175184...3945.0100523537.8825430.0Gentoo penguin (Pygoscelis papua)Biscoe44.013.6208.04350.0FEMALE
\n", + "

10 rows × 12 columns

\n", + "
[168 rows x 12 columns in total]" + ], + "text/plain": [ + " predicted_label top_feature_attributions \\\n", + "0 3945.010052 [{'feature': 'island', 'attribution': 0.0}\n", + " {'... \n", + "1 3914.916297 [{'feature': 'flipper_length_mm', 'attribution... \n", + "2 3278.611224 [{'feature': 'sex', 'attribution': -443.175184... \n", + "3 4006.367355 [{'feature': 'culmen_length_mm', 'attribution'... \n", + "4 3417.610478 [{'feature': 'sex', 'attribution': -443.175184... \n", + "5 4009.612421 [{'feature': 'culmen_length_mm', 'attribution'... \n", + "6 4231.330911 [{'feature': 'flipper_length_mm', 'attribution... \n", + "7 3554.308906 [{'feature': 'sex', 'attribution': -443.175184... \n", + "8 3550.677455 [{'feature': 'sex', 'attribution': -443.175184... \n", + "9 3537.882543 [{'feature': 'sex', 'attribution': -443.175184... \n", + "\n", + " baseline_prediction_value prediction_value approximation_error \\\n", + "0 3945.010052 3945.010052 0.0 \n", + "1 3945.010052 3914.916297 0.0 \n", + "2 3945.010052 3278.611224 0.0 \n", + "3 3945.010052 4006.367355 0.0 \n", + "4 3945.010052 3417.610478 0.0 \n", + "5 3945.010052 4009.612421 0.0 \n", + "6 3945.010052 4231.330911 0.0 \n", + "7 3945.010052 3554.308906 0.0 \n", + "8 3945.010052 3550.677455 0.0 \n", + "9 3945.010052 3537.882543 0.0 \n", + "\n", + " species island culmen_length_mm \\\n", + "0 Gentoo penguin (Pygoscelis papua) Biscoe \n", + "1 Adelie Penguin (Pygoscelis adeliae) Biscoe 39.7 \n", + "2 Adelie Penguin (Pygoscelis adeliae) Biscoe 36.4 \n", + "3 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.6 \n", + "4 Adelie Penguin (Pygoscelis adeliae) Biscoe 35.0 \n", + "5 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.1 \n", + "6 Adelie Penguin (Pygoscelis adeliae) Biscoe 42.0 \n", + "7 Gentoo penguin (Pygoscelis papua) Biscoe 43.8 \n", + "8 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", + "9 Gentoo penguin (Pygoscelis papua) Biscoe 44.0 \n", + "\n", + " culmen_depth_mm flipper_length_mm body_mass_g sex \n", + "0 \n", + "1 18.9 184.0 3550.0 MALE \n", + "2 17.1 184.0 2850.0 FEMALE \n", + "3 18.0 192.0 3950.0 MALE \n", + "4 17.9 192.0 3725.0 FEMALE \n", + "5 18.2 192.0 4050.0 MALE \n", + "6 19.5 200.0 4050.0 MALE \n", + "7 13.9 208.0 4300.0 FEMALE \n", + "8 14.0 208.0 4575.0 FEMALE \n", + "9 13.6 208.0 4350.0 FEMALE \n", + "...\n", + "\n", + "[168 rows x 12 columns]" ] - }, + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.ml.explain_predict(model_name, biscoe, top_k_features=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K0mPaoGpcwwy" + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Globally explain the model\n", + "\n", + "To know which features are generally the most important to determine penguin\n", + "weight, you can use the `global_explain` function. In order to use\n", + "`global_explain`, you must retrain the model with the `enable_global_explain`\n", + "option set to `True`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "ZSP7gt13QrQt" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "id": "kGBJKafpo0dl" - }, - "outputs": [ - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 0 Bytes in a moment of slot time.\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", - "

1 rows × 6 columns

\n", - "
[1 rows x 6 columns in total]" - ], - "text/plain": [ - " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - "0 223.878763 78553.601634 0.005614 \n", - "\n", - " median_absolute_error r2_score explained_variance \n", - "0 181.330911 0.623951 0.623951 \n", - "\n", - "[1 rows x 6 columns]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 6.9 kB in 53 seconds of slot time. [Job bigframes-dev:US.job_welN8ErlZ_sTG7oOEULsWUgmIg7l details]\n", + " " ], - "source": [ - "bbq.ml.evaluate(model_name)" + "text/plain": [ + "" ] - }, + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model_name = f\"{PROJECT_ID}.{DATASET_ID}.penguin_weight_with_global_explain\"\n", + "model_metadata = bbq.ml.create_model(\n", + " model_name,\n", + " replace=True,\n", + " options={\n", + " \"model_type\": \"LINEAR_REG\",\n", + " \"input_label_cols\": [\"body_mass_g\"],\n", + " \"enable_global_explain\": True,\n", + " },\n", + " training_data=training_data,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "P2lUiZZ_cjri" - }, - "source": [ - "### Use the model to predict outcomes\n", - "\n", - "Now that you have evaluated your model, the next step is to use it to predict an\n", - "outcome. You can run `bigframes.bigquery.ml.predict` function on the model to\n", - "predict the body mass in grams of all penguins that reside on the Biscoe\n", - "Islands." + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 0 Bytes in a moment of slot time.\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "id": "bsQ9cmoWo0Ps" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/swast/src/github.com/googleapis/python-bigquery-dataframes/bigframes/core/log_adapter.py:182: TimeTravelCacheWarning: Reading cached table from 2025-12-03 16:30:18.272882+00:00 to avoid\n", - "incompatibilies with previous reads of this table. To read the latest\n", - "version, set `use_cache=False` or close the current session with\n", - "Session.close() or bigframes.pandas.close_session().\n", - " return method(*args, **kwargs)\n" - ] - }, - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 29.3 kB in a moment of slot time.\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - "
predicted_labelspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
03945.010052Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
13914.916297Adelie Penguin (Pygoscelis adeliae)Biscoe39.718.9184.03550.0MALE
23278.611224Adelie Penguin (Pygoscelis adeliae)Biscoe36.417.1184.02850.0FEMALE
34006.367355Adelie Penguin (Pygoscelis adeliae)Biscoe41.618.0192.03950.0MALE
43417.610478Adelie Penguin (Pygoscelis adeliae)Biscoe35.017.9192.03725.0FEMALE
54009.612421Adelie Penguin (Pygoscelis adeliae)Biscoe41.118.2192.04050.0MALE
64231.330911Adelie Penguin (Pygoscelis adeliae)Biscoe42.019.5200.04050.0MALE
73554.308906Gentoo penguin (Pygoscelis papua)Biscoe43.813.9208.04300.0FEMALE
83550.677455Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
93537.882543Gentoo penguin (Pygoscelis papua)Biscoe44.013.6208.04350.0FEMALE
\n", - "

10 rows × 8 columns

\n", - "
[168 rows x 8 columns in total]" - ], - "text/plain": [ - " predicted_label species island \\\n", - "0 3945.010052 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "1 3914.916297 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "2 3278.611224 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "3 4006.367355 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "4 3417.610478 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "5 4009.612421 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "6 4231.330911 Adelie Penguin (Pygoscelis adeliae) Biscoe \n", - "7 3554.308906 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "8 3550.677455 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "9 3537.882543 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "\n", - " culmen_length_mm culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 \n", - "1 39.7 18.9 184.0 3550.0 MALE \n", - "2 36.4 17.1 184.0 2850.0 FEMALE \n", - "3 41.6 18.0 192.0 3950.0 MALE \n", - "4 35.0 17.9 192.0 3725.0 FEMALE \n", - "5 41.1 18.2 192.0 4050.0 MALE \n", - "6 42.0 19.5 200.0 4050.0 MALE \n", - "7 43.8 13.9 208.0 4300.0 FEMALE \n", - "8 43.3 14.0 208.0 4575.0 FEMALE \n", - "9 44.0 13.6 208.0 4350.0 FEMALE \n", - "...\n", - "\n", - "[168 rows x 8 columns]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. " ], - "source": [ - "df = bpd.read_gbq(\"bigquery-public-data.ml_datasets.penguins\")\n", - "biscoe = df[df[\"island\"].str.contains(\"Biscoe\")]\n", - "bbq.ml.predict(model_name, biscoe)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "GTRdUw-Ro5R1" - }, - "source": [ - "### Explain the prediction results\n", - "\n", - "To understand why the model is generating these prediction results, you can use the `explain_predict` function." + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " Query started with request ID bigframes-dev:US.161bba69-c852-4916-a2df-bb5b309be6e4.
SQL
SELECT * FROM ML.EXPLAIN_PREDICT(MODEL `bigframes-dev.bqml_tutorial.penguin_weight`, (SELECT\n",
-              "`bfuid_col_22` AS `species`,\n",
-              "`bfuid_col_23` AS `island`,\n",
-              "`bfuid_col_24` AS `culmen_length_mm`,\n",
-              "`bfuid_col_25` AS `culmen_depth_mm`,\n",
-              "`bfuid_col_26` AS `flipper_length_mm`,\n",
-              "`bfuid_col_27` AS `body_mass_g`,\n",
-              "`bfuid_col_28` AS `sex`\n",
-              "FROM\n",
-              "(SELECT\n",
-              "  `t0`.`species`,\n",
-              "  `t0`.`island`,\n",
-              "  `t0`.`culmen_length_mm`,\n",
-              "  `t0`.`culmen_depth_mm`,\n",
-              "  `t0`.`flipper_length_mm`,\n",
-              "  `t0`.`body_mass_g`,\n",
-              "  `t0`.`sex`,\n",
-              "  `t0`.`species` AS `bfuid_col_22`,\n",
-              "  `t0`.`island` AS `bfuid_col_23`,\n",
-              "  `t0`.`culmen_length_mm` AS `bfuid_col_24`,\n",
-              "  `t0`.`culmen_depth_mm` AS `bfuid_col_25`,\n",
-              "  `t0`.`flipper_length_mm` AS `bfuid_col_26`,\n",
-              "  `t0`.`body_mass_g` AS `bfuid_col_27`,\n",
-              "  `t0`.`sex` AS `bfuid_col_28`,\n",
-              "  regexp_contains(`t0`.`island`, 'Biscoe') AS `bfuid_col_29`\n",
-              "FROM (\n",
-              "  SELECT\n",
-              "    `species`,\n",
-              "    `island`,\n",
-              "    `culmen_length_mm`,\n",
-              "    `culmen_depth_mm`,\n",
-              "    `flipper_length_mm`,\n",
-              "    `body_mass_g`,\n",
-              "    `sex`\n",
-              "  FROM `bigquery-public-data.ml_datasets.penguins` FOR SYSTEM_TIME AS OF TIMESTAMP('2025-12-03T16:30:18.272882+00:00')\n",
-              ") AS `t0`\n",
-              "WHERE\n",
-              "  regexp_contains(`t0`.`island`, 'Biscoe'))), STRUCT(3 AS top_k_features))\n",
-              "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - " \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", - " \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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
predicted_labeltop_feature_attributionsbaseline_prediction_valueprediction_valueapproximation_errorspeciesislandculmen_length_mmculmen_depth_mmflipper_length_mmbody_mass_gsex
03945.010052[{'feature': 'island', 'attribution': 0.0}\n", - " {'...3945.0100523945.0100520.0Gentoo penguin (Pygoscelis papua)Biscoe<NA><NA><NA><NA><NA>
13914.916297[{'feature': 'flipper_length_mm', 'attribution...3945.0100523914.9162970.0Adelie Penguin (Pygoscelis adeliae)Biscoe39.718.9184.03550.0MALE
23278.611224[{'feature': 'sex', 'attribution': -443.175184...3945.0100523278.6112240.0Adelie Penguin (Pygoscelis adeliae)Biscoe36.417.1184.02850.0FEMALE
34006.367355[{'feature': 'culmen_length_mm', 'attribution'...3945.0100524006.3673550.0Adelie Penguin (Pygoscelis adeliae)Biscoe41.618.0192.03950.0MALE
43417.610478[{'feature': 'sex', 'attribution': -443.175184...3945.0100523417.6104780.0Adelie Penguin (Pygoscelis adeliae)Biscoe35.017.9192.03725.0FEMALE
54009.612421[{'feature': 'culmen_length_mm', 'attribution'...3945.0100524009.6124210.0Adelie Penguin (Pygoscelis adeliae)Biscoe41.118.2192.04050.0MALE
64231.330911[{'feature': 'flipper_length_mm', 'attribution...3945.0100524231.3309110.0Adelie Penguin (Pygoscelis adeliae)Biscoe42.019.5200.04050.0MALE
73554.308906[{'feature': 'sex', 'attribution': -443.175184...3945.0100523554.3089060.0Gentoo penguin (Pygoscelis papua)Biscoe43.813.9208.04300.0FEMALE
83550.677455[{'feature': 'sex', 'attribution': -443.175184...3945.0100523550.6774550.0Gentoo penguin (Pygoscelis papua)Biscoe43.314.0208.04575.0FEMALE
93537.882543[{'feature': 'sex', 'attribution': -443.175184...3945.0100523537.8825430.0Gentoo penguin (Pygoscelis papua)Biscoe44.013.6208.04350.0FEMALE
\n", - "

10 rows × 12 columns

\n", - "
[168 rows x 12 columns in total]" - ], - "text/plain": [ - " predicted_label top_feature_attributions \\\n", - "0 3945.010052 [{'feature': 'island', 'attribution': 0.0}\n", - " {'... \n", - "1 3914.916297 [{'feature': 'flipper_length_mm', 'attribution... \n", - "2 3278.611224 [{'feature': 'sex', 'attribution': -443.175184... \n", - "3 4006.367355 [{'feature': 'culmen_length_mm', 'attribution'... \n", - "4 3417.610478 [{'feature': 'sex', 'attribution': -443.175184... \n", - "5 4009.612421 [{'feature': 'culmen_length_mm', 'attribution'... \n", - "6 4231.330911 [{'feature': 'flipper_length_mm', 'attribution... \n", - "7 3554.308906 [{'feature': 'sex', 'attribution': -443.175184... \n", - "8 3550.677455 [{'feature': 'sex', 'attribution': -443.175184... \n", - "9 3537.882543 [{'feature': 'sex', 'attribution': -443.175184... \n", - "\n", - " baseline_prediction_value prediction_value approximation_error \\\n", - "0 3945.010052 3945.010052 0.0 \n", - "1 3945.010052 3914.916297 0.0 \n", - "2 3945.010052 3278.611224 0.0 \n", - "3 3945.010052 4006.367355 0.0 \n", - "4 3945.010052 3417.610478 0.0 \n", - "5 3945.010052 4009.612421 0.0 \n", - "6 3945.010052 4231.330911 0.0 \n", - "7 3945.010052 3554.308906 0.0 \n", - "8 3945.010052 3550.677455 0.0 \n", - "9 3945.010052 3537.882543 0.0 \n", - "\n", - " species island culmen_length_mm \\\n", - "0 Gentoo penguin (Pygoscelis papua) Biscoe \n", - "1 Adelie Penguin (Pygoscelis adeliae) Biscoe 39.7 \n", - "2 Adelie Penguin (Pygoscelis adeliae) Biscoe 36.4 \n", - "3 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.6 \n", - "4 Adelie Penguin (Pygoscelis adeliae) Biscoe 35.0 \n", - "5 Adelie Penguin (Pygoscelis adeliae) Biscoe 41.1 \n", - "6 Adelie Penguin (Pygoscelis adeliae) Biscoe 42.0 \n", - "7 Gentoo penguin (Pygoscelis papua) Biscoe 43.8 \n", - "8 Gentoo penguin (Pygoscelis papua) Biscoe 43.3 \n", - "9 Gentoo penguin (Pygoscelis papua) Biscoe 44.0 \n", - "\n", - " culmen_depth_mm flipper_length_mm body_mass_g sex \n", - "0 \n", - "1 18.9 184.0 3550.0 MALE \n", - "2 17.1 184.0 2850.0 FEMALE \n", - "3 18.0 192.0 3950.0 MALE \n", - "4 17.9 192.0 3725.0 FEMALE \n", - "5 18.2 192.0 4050.0 MALE \n", - "6 19.5 200.0 4050.0 MALE \n", - "7 13.9 208.0 4300.0 FEMALE \n", - "8 14.0 208.0 4575.0 FEMALE \n", - "9 13.6 208.0 4350.0 FEMALE \n", - "...\n", - "\n", - "[168 rows x 12 columns]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
featureattribution
0sex221.587592
1flipper_length_mm71.311846
2culmen_depth_mm66.17986
3culmen_length_mm45.443363
4island17.258076
\n", + "

5 rows × 2 columns

\n", + "
[5 rows x 2 columns in total]" ], - "source": [ - "bbq.ml.explain_predict(model_name, biscoe, top_k_features=3)" + "text/plain": [ + " feature attribution\n", + "0 sex 221.587592\n", + "1 flipper_length_mm 71.311846\n", + "2 culmen_depth_mm 66.17986\n", + "3 culmen_length_mm 45.443363\n", + "4 island 17.258076\n", + "\n", + "[5 rows x 2 columns]" ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "K0mPaoGpcwwy" - }, - "source": [] - }, + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.ml.global_explain(model_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compatibility with pandas\n", + "\n", + "The functions in `bigframes.bigquery.ml` can accept pandas DataFrames as well. Use the `to_pandas()` method on the results of methods like `predict()` to get a pandas DataFrame back." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Globally explain the model\n", - "\n", - "To know which features are generally the most important to determine penguin\n", - "weight, you can use the `global_explain` function. In order to use\n", - "`global_explain`, you must retrain the model with the `enable_global_explain`\n", - "option set to `True`." + "data": { + "text/html": [ + "\n", + " Query started with request ID bigframes-dev:US.18d9027b-7d55-42c9-ad1b-dabccdda80dc.
SQL
SELECT * FROM ML.PREDICT(MODEL `bigframes-dev.bqml_tutorial.penguin_weight_with_global_explain`, (SELECT\n",
+       "`column_0` AS `sex`,\n",
+       "`column_1` AS `flipper_length_mm`,\n",
+       "`column_2` AS `culmen_depth_mm`,\n",
+       "`column_3` AS `culmen_length_mm`,\n",
+       "`column_4` AS `island`\n",
+       "FROM\n",
+       "(SELECT\n",
+       "  *\n",
+       "FROM (\n",
+       "  SELECT\n",
+       "    *\n",
+       "  FROM UNNEST(ARRAY<STRUCT<`column_0` STRING, `column_1` INT64, `column_2` INT64, `column_3` INT64, `column_4` STRING>>[STRUCT('MALE', 180, 15, 40, 'Biscoe'), STRUCT('FEMALE', 190, 16, 41, 'Biscoe'), STRUCT('MALE', 200, 17, 42, 'Dream'), STRUCT('FEMALE', 210, 18, 43, 'Dream')]) AS `column_0`\n",
+       ") AS `t0`)))\n",
+       "
\n", + " " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "id": "ZSP7gt13QrQt" - }, - "outputs": [ - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 6.9 kB in 53 seconds of slot time. [Job bigframes-dev:US.job_welN8ErlZ_sTG7oOEULsWUgmIg7l details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "data": { + "text/html": [ + "✅ Completed. " ], - "source": [ - "model_name = f\"{PROJECT_ID}.{DATASET_ID}.penguin_weight_with_global_explain\"\n", - "model_metadata = bbq.ml.create_model(\n", - " model_name,\n", - " replace=True,\n", - " options={\n", - " \"model_type\": \"LINEAR_REG\",\n", - " \"input_label_cols\": [\"body_mass_g\"],\n", - " \"enable_global_explain\": True,\n", - " },\n", - " training_data=training_data,\n", - ")" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 0 Bytes in a moment of slot time.\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "index", + "rawType": "Int64", + "type": "integer" }, { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "predicted_body_mass_g", + "rawType": "Float64", + "type": "float" }, { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "sex", + "rawType": "string", + "type": "string" }, { - "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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
featureattribution
0sex221.587592
1flipper_length_mm71.311846
2culmen_depth_mm66.17986
3culmen_length_mm45.443363
4island17.258076
\n", - "

5 rows × 2 columns

\n", - "
[5 rows x 2 columns in total]" - ], - "text/plain": [ - " feature attribution\n", - "0 sex 221.587592\n", - "1 flipper_length_mm 71.311846\n", - "2 culmen_depth_mm 66.17986\n", - "3 culmen_length_mm 45.443363\n", - "4 island 17.258076\n", - "\n", - "[5 rows x 2 columns]" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bbq.ml.global_explain(model_name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Compatibility with pandas\n", - "\n", - "The functions in `bigframes.bigquery.ml` can accept pandas DataFrames as well. Use the `to_pandas()` method on the results of methods like `predict()` to get a pandas DataFrame back." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ + "name": "flipper_length_mm", + "rawType": "Int64", + "type": "integer" + }, { - "data": { - "text/html": [ - "\n", - " Query started with request ID bigframes-dev:US.18d9027b-7d55-42c9-ad1b-dabccdda80dc.
SQL
SELECT * FROM ML.PREDICT(MODEL `bigframes-dev.bqml_tutorial.penguin_weight_with_global_explain`, (SELECT\n",
-              "`column_0` AS `sex`,\n",
-              "`column_1` AS `flipper_length_mm`,\n",
-              "`column_2` AS `culmen_depth_mm`,\n",
-              "`column_3` AS `culmen_length_mm`,\n",
-              "`column_4` AS `island`\n",
-              "FROM\n",
-              "(SELECT\n",
-              "  *\n",
-              "FROM (\n",
-              "  SELECT\n",
-              "    *\n",
-              "  FROM UNNEST(ARRAY<STRUCT<`column_0` STRING, `column_1` INT64, `column_2` INT64, `column_3` INT64, `column_4` STRING>>[STRUCT('MALE', 180, 15, 40, 'Biscoe'), STRUCT('FEMALE', 190, 16, 41, 'Biscoe'), STRUCT('MALE', 200, 17, 42, 'Dream'), STRUCT('FEMALE', 210, 18, 43, 'Dream')]) AS `column_0`\n",
-              ") AS `t0`)))\n",
-              "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "culmen_depth_mm", + "rawType": "Int64", + "type": "integer" }, { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "culmen_length_mm", + "rawType": "Int64", + "type": "integer" }, { - "data": { - "application/vnd.microsoft.datawrangler.viewer.v0+json": { - "columns": [ - { - "name": "index", - "rawType": "Int64", - "type": "integer" - }, - { - "name": "predicted_body_mass_g", - "rawType": "Float64", - "type": "float" - }, - { - "name": "sex", - "rawType": "string", - "type": "string" - }, - { - "name": "flipper_length_mm", - "rawType": "Int64", - "type": "integer" - }, - { - "name": "culmen_depth_mm", - "rawType": "Int64", - "type": "integer" - }, - { - "name": "culmen_length_mm", - "rawType": "Int64", - "type": "integer" - }, - { - "name": "island", - "rawType": "string", - "type": "string" - } - ], - "ref": "01d67015-64b6-463e-8c16-e8ac1363ff67", - "rows": [ - [ - "0", - "3596.332210728767", - "MALE", - "180", - "15", - "40", - "Biscoe" - ], - [ - "1", - "3384.6999176328636", - "FEMALE", - "190", - "16", - "41", - "Biscoe" - ], - [ - "2", - "4049.581795919061", - "MALE", - "200", - "17", - "42", - "Dream" - ], - [ - "3", - "3837.9495028231568", - "FEMALE", - "210", - "18", - "43", - "Dream" - ] - ], - "shape": { - "columns": 6, - "rows": 4 - } - }, - "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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
predicted_body_mass_gsexflipper_length_mmculmen_depth_mmculmen_length_mmisland
03596.332211MALE1801540Biscoe
13384.699918FEMALE1901641Biscoe
24049.581796MALE2001742Dream
33837.949503FEMALE2101843Dream
\n", - "
" - ], - "text/plain": [ - " predicted_body_mass_g sex flipper_length_mm culmen_depth_mm \\\n", - "0 3596.332211 MALE 180 15 \n", - "1 3384.699918 FEMALE 190 16 \n", - "2 4049.581796 MALE 200 17 \n", - "3 3837.949503 FEMALE 210 18 \n", - "\n", - " culmen_length_mm island \n", - "0 40 Biscoe \n", - "1 41 Biscoe \n", - "2 42 Dream \n", - "3 43 Dream " - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" + "name": "island", + "rawType": "string", + "type": "string" } + ], + "ref": "01d67015-64b6-463e-8c16-e8ac1363ff67", + "rows": [ + [ + "0", + "3596.332210728767", + "MALE", + "180", + "15", + "40", + "Biscoe" + ], + [ + "1", + "3384.6999176328636", + "FEMALE", + "190", + "16", + "41", + "Biscoe" + ], + [ + "2", + "4049.581795919061", + "MALE", + "200", + "17", + "42", + "Dream" + ], + [ + "3", + "3837.9495028231568", + "FEMALE", + "210", + "18", + "43", + "Dream" + ] + ], + "shape": { + "columns": 6, + "rows": 4 + } + }, + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
predicted_body_mass_gsexflipper_length_mmculmen_depth_mmculmen_length_mmisland
03596.332211MALE1801540Biscoe
13384.699918FEMALE1901641Biscoe
24049.581796MALE2001742Dream
33837.949503FEMALE2101843Dream
\n", + "
" ], - "source": [ - "import pandas as pd\n", - "\n", - "predict_df = pd.DataFrame({\n", - " \"sex\": [\"MALE\", \"FEMALE\", \"MALE\", \"FEMALE\"],\n", - " \"flipper_length_mm\": [180, 190, 200, 210],\n", - " \"culmen_depth_mm\": [15, 16, 17, 18],\n", - " \"culmen_length_mm\": [40, 41, 42, 43],\n", - " \"island\": [\"Biscoe\", \"Biscoe\", \"Dream\", \"Dream\"],\n", - "})\n", - "bbq.ml.predict(model_metadata, predict_df).to_pandas()" + "text/plain": [ + " predicted_body_mass_g sex flipper_length_mm culmen_depth_mm \\\n", + "0 3596.332211 MALE 180 15 \n", + "1 3384.699918 FEMALE 190 16 \n", + "2 4049.581796 MALE 200 17 \n", + "3 3837.949503 FEMALE 210 18 \n", + "\n", + " culmen_length_mm island \n", + "0 40 Biscoe \n", + "1 41 Biscoe \n", + "2 42 Dream \n", + "3 43 Dream " ] - }, + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "predict_df = pd.DataFrame({\n", + " \"sex\": [\"MALE\", \"FEMALE\", \"MALE\", \"FEMALE\"],\n", + " \"flipper_length_mm\": [180, 190, 200, 210],\n", + " \"culmen_depth_mm\": [15, 16, 17, 18],\n", + " \"culmen_length_mm\": [40, 41, 42, 43],\n", + " \"island\": [\"Biscoe\", \"Biscoe\", \"Dream\", \"Dream\"],\n", + "})\n", + "bbq.ml.predict(model_metadata, predict_df).to_pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Compatibility with `bigframes.ml`\n", + "\n", + "The models created with `bigframes.bigquery.ml` can be used with the scikit-learn-like `bigframes.ml` modules by using the `read_gbq_model` method.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Compatibility with `bigframes.ml`\n", - "\n", - "The models created with `bigframes.bigquery.ml` can be used with the scikit-learn-like `bigframes.ml` modules by using the `read_gbq_model` method.\n" + "data": { + "text/plain": [ + "LinearRegression(enable_global_explain=True,\n", + " optimize_strategy='NORMAL_EQUATION')" ] - }, + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model = bpd.read_gbq_model(model_name)\n", + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "LinearRegression(enable_global_explain=True,\n", - " optimize_strategy='NORMAL_EQUATION')" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. \n", + " Query processed 7.3 kB in a moment of slot time. [Job bigframes-dev:US.f2f86927-bbd1-431d-b89e-3d6a064268d7 details]\n", + " " ], - "source": [ - "model = bpd.read_gbq_model(model_name)\n", - "model" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "✅ Completed. \n", - " Query processed 7.3 kB in a moment of slot time. [Job bigframes-dev:US.f2f86927-bbd1-431d-b89e-3d6a064268d7 details]\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "✅ Completed. " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "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", - "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", - "

1 rows × 6 columns

\n", - "
[1 rows x 6 columns in total]" - ], - "text/plain": [ - " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", - " 223.878763 78553.601634 0.005614 \n", - "\n", - " median_absolute_error r2_score explained_variance \n", - " 181.330911 0.623951 0.623951 \n", - "\n", - "[1 rows x 6 columns]" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "✅ Completed. " ], - "source": [ - "X = training_data[[\"sex\", \"flipper_length_mm\", \"culmen_depth_mm\", \"culmen_length_mm\", \"island\"]]\n", - "y = training_data[[\"body_mass_g\"]]\n", - "model.score(X, y)" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "G_wjSfXpWTuy" - }, - "source": [ - "# Summary and next steps\n", - "\n", - "You've created a linear regression model using `bigframes.bigquery.ml`.\n", - "\n", - "Learn more about BigQuery DataFrames in the [documentation](https://dataframes.bigquery.dev/) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + "data": { + "text/html": [ + "✅ Completed. " + ], + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "# # Delete the BigQuery dataset and associated ML model\n", - "# from google.cloud import bigquery\n", - "# client = bigquery.Client(project=PROJECT_ID)\n", - "# client.delete_dataset(\n", - "# DATASET_ID, delete_contents=True, not_found_ok=True\n", - "# )\n", - "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" + "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", + "
mean_absolute_errormean_squared_errormean_squared_log_errormedian_absolute_errorr2_scoreexplained_variance
0223.87876378553.6016340.005614181.3309110.6239510.623951
\n", + "

1 rows × 6 columns

\n", + "
[1 rows x 6 columns in total]" + ], + "text/plain": [ + " mean_absolute_error mean_squared_error mean_squared_log_error \\\n", + " 223.878763 78553.601634 0.005614 \n", + "\n", + " median_absolute_error r2_score explained_variance \n", + " 181.330911 0.623951 0.623951 \n", + "\n", + "[1 rows x 6 columns]" ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" } - ], - "metadata": { - "colab": { - "provenance": [], - "toc_visible": true - }, - "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.9" - } + ], + "source": [ + "X = training_data[[\"sex\", \"flipper_length_mm\", \"culmen_depth_mm\", \"culmen_length_mm\", \"island\"]]\n", + "y = training_data[[\"body_mass_g\"]]\n", + "model.score(X, y)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G_wjSfXpWTuy" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "You've created a linear regression model using `bigframes.bigquery.ml`.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://dataframes.bigquery.dev/) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery dataset and associated ML model\n", + "# from google.cloud import bigquery\n", + "# client = bigquery.Client(project=PROJECT_ID)\n", + "# client.delete_dataset(\n", + "# DATASET_ID, delete_contents=True, not_found_ok=True\n", + "# )\n", + "# print(\"Deleted dataset '{}'.\".format(DATASET_ID))" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/ml/bq_dataframes_ml_linear_regression_big.ipynb b/notebooks/ml/bq_dataframes_ml_linear_regression_big.ipynb index 5c016f9157d..d286f5ce31d 100644 --- a/notebooks/ml/bq_dataframes_ml_linear_regression_big.ipynb +++ b/notebooks/ml/bq_dataframes_ml_linear_regression_big.ipynb @@ -1,1064 +1,1064 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ur8xi4C7S06n" - }, - "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": { - "id": "JAPoU8Sm5E6e" - }, - "source": [ - "## Train a linear regression model with BigQuery DataFrames ML\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"Vertex\n", - " Open in Vertex AI Workbench\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "24743cf4a1e1" - }, - "source": [ - "**_NOTE_**: This notebook has been tested in the following environment:\n", - "\n", - "* Python version = 3.11" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tvgnzT1CKxrO" - }, - "source": [ - "## Overview\n", - "\n", - "This notebook demonstrates training a linear regression model on Big Data using BigQuery DataFrames ML. BigQuery DataFrames ML provides a provides a scikit-learn-like API for ML powered by the BigQuery engine.\n", - "\n", - "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d975e698c9a4" - }, - "source": [ - "### Objective\n", - "\n", - "In this tutorial, we use BigQuery DataFrames to create a linear regression model that predicts the levels of Ozone in the atmosphere.\n", - "\n", - "The steps include:\n", - "\n", - "- Creating a DataFrame from the BigQuery table.\n", - "- Cleaning and preparing data using `bigframes.pandas` module.\n", - "- Creating a linear regression model using `bigframes.ml` module.\n", - "- Saving the ML model to BigQuery for future use.\n", - "\n", - "\n", - "Let's formally define our problem as: **Train a linear regression model to predict the level of ozone in the atmosphere given the measurements of other constituents and properties of the atmosphere.**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "08d289fa873f" - }, - "source": [ - "### Dataset\n", - "\n", - "In this tutorial we are going to use the [`bigquery-public-data.epa_historical_air_quality`](https://console.cloud.google.com/marketplace/product/epa/historical-air-quality) dataset. To quote the description of the dataset:\n", - "\n", - "\"The United States Environmental Protection Agency (EPA) protects both public health and the environment by establishing the standards for national air quality. The EPA provides annual summary data as well as hourly and daily data in the categories of criteria gases, particulates, meteorological, and toxics.\"\n", - "\n", - "There are several tables capturing data about the constituents of the atmosphere, see them in the [BigQuery cloud console](https://pantheon.corp.google.com/bigquery?p=bigquery-public-data&d=epa_historical_air_quality&page=dataset). Most tables carry 10's of GBs of data, but that is not an issue with BigQuery DataFrames as the data is efficiently processed at BigQuery without transferring them to the client." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "aed92deeb4a0" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "* BigQuery ML\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", - "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "i7EUnXsZhAGF" - }, - "source": [ - "## Installation\n", - "\n", - "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", - "\n", - "1. Install the package\n", - "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9O0Ka4W2MNF3" - }, - "outputs": [], - "source": [ - "# !pip install bigframes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "f200f10a1da3" - }, - "outputs": [], - "source": [ - "# Automatically restart kernel after installs so that your environment can access the new packages\n", - "\n", - "# import IPython\n", - "#\n", - "# app = IPython.Application.instance()\n", - "# app.kernel.do_shutdown(True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BF1j6f9HApxa" - }, - "source": [ - "## Before you begin\n", - "\n", - "Complete the tasks in this section to set up your environment." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "oDfTjfACBvJk" - }, - "source": [ - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WReHDGG5g0XY" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "If you don't know your project ID, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "oM1iC_MfAts1" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "region" - }, - "source": [ - "#### Set the BigQuery location\n", - "\n", - "You can also change the `LOCATION` variable used by BigQuery. Learn more about [BigQuery locations](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "eF-Twtc4XGem" - }, - "outputs": [], - "source": [ - "LOCATION = \"US\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sBCra4QMA2wR" - }, - "source": [ - "### Set up APIs, IAM permissions and Authentication\n", - "\n", - "Follow the instructions at https://cloud.google.com/bigquery/docs/use-bigquery-dataframes#permissions.\n", - "\n", - "Depending on your notebook environment, you might have to manually authenticate. Follow the relevant instructions below." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "74ccc9e52986" - }, - "source": [ - "**Vertex AI Workbench**\n", - "\n", - "Do nothing, you are already authenticated." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "de775a3773ba" - }, - "source": [ - "**Local JupyterLab instance**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "254614fa0c46" - }, - "outputs": [], - "source": [ - "# ! gcloud auth login\n", - "# ! gcloud auth application-default login" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ef21552ccea8" - }, - "source": [ - "**Colab**\n", - "\n", - "Uncomment and run the following cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "603adbbf0532" - }, - "outputs": [], - "source": [ - "# from google.colab import auth\n", - "# auth.authenticate_user()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "960505627ddf" - }, - "source": [ - "### Import libraries" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "PyQmSRbKA8r-" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bpd" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "init_aip:mbsdk,all" - }, - "source": [ - "### Set BigQuery DataFrames options" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NPPMuw2PXGeo" - }, - "outputs": [], - "source": [ - "# NOTE: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bpd.options.bigquery.project = PROJECT_ID\n", - "\n", - "# NOTE: The location option is not required.\n", - "# It defaults to the location of the first table or query\n", - "# passed to read_gbq(). For APIs where a location can't be\n", - "# auto-detected, the location defaults to the \"US\" location.\n", - "bpd.options.bigquery.location = LOCATION\n", - "\n", - "# NOTE: For a machine learning model the order of the data is\n", - "# not important. So let's relax the ordering_mode to accept\n", - "# partial ordering. This allows BigQuery DataFrames to run cost\n", - "# and performance optimized jobs at the BigQuery engine.\n", - "bpd.options.bigquery.ordering_mode = \"partial\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "D21CoOlfFTYI" - }, - "source": [ - "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9EMAqR37AfLS" - }, - "source": [ - "## Read data in BigQuery tables as DataFrame\n", - "\n", - "Let's read the tables in the dataset to construct a BigQuery DataFrames DataFrame. We will combine measurements of various parameters of the atmosphere from multiple tables to represent a consolidated dataframe to use for our model training and prediction. We have daily and hourly versions of the data available, but since we want to create a model that is dynamic so that it can capture the variance throughout the day, we would choose the hourly version.\n", - "\n", - "Note that we would use the pandas APIs as we normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of the local environment." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dataset = \"bigquery-public-data.epa_historical_air_quality\"\n", - "hourly_summary_tables = [\n", - " \"co_hourly_summary\",\n", - " \"hap_hourly_summary\",\n", - " \"no2_hourly_summary\",\n", - " \"nonoxnoy_hourly_summary\",\n", - " \"o3_hourly_summary\",\n", - " \"pm10_hourly_summary\",\n", - " \"pm25_frm_hourly_summary\",\n", - " \"pm25_nonfrm_hourly_summary\",\n", - " \"pm25_speciation_hourly_summary\",\n", - " \"pressure_hourly_summary\",\n", - " \"rh_and_dp_hourly_summary\",\n", - " \"so2_hourly_summary\",\n", - " \"temperature_hourly_summary\",\n", - " \"voc_hourly_summary\",\n", - " \"wind_hourly_summary\",\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's pick index columns - to identify a measurement of the atmospheric parameter, param column - to identify which param the measurement pertains to, and value column - the column containing the measurement itself." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "index_columns = [\"state_name\", \"county_name\", \"site_num\", \"date_local\", \"time_local\"]\n", - "param_column = \"parameter_name\"\n", - "value_column = \"sample_measurement\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's observe how much data each table contains:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for table in hourly_summary_tables:\n", - " # get the bigframes global session\n", - " bigframes_session = bpd.get_global_session()\n", - "\n", - " # get the bigquery table info\n", - " table_info = bigframes_session.bqclient.get_table(f\"{dataset}.{table}\")\n", - "\n", - " # read the table as a dataframe\n", - " df = bpd.read_gbq(f\"{dataset}.{table}\")\n", - "\n", - " # print metadata about the table\n", - " print(\n", - " f\"{table}: \"\n", - " f\"{round(table_info.num_bytes/1_000_000_000, 1)} GB, \"\n", - " f\"{round(table_info.num_rows/1_000_000, 1)} million rows, \"\n", - " f\"{df[param_column].nunique()} params\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's be mindful that the rows in each table may contain duplicates, which may introdude bias in any model trained on the raw data. We will make sure to drop the duplicates when we use the data for model training.\n", - "\n", - "Since we want to predict ozone level, we obviously pick the `o3` table. Let's also pick the tables about other gases - `co`, `no2` and `so2`. Let's also pick `pressure` and `temperature` tables as they seem fundamental indicators for the atmosphere. Note that each of these tables capture measurements for a single parameter (i.e. the column `parameter_name` has a single unique value).\n", - "\n", - "We are also interested in the nonoxny and wind tables, but they capture multiple parameters (i.e. the column `parameter_name` has a more than one unique values). We will include their measurements in later step, as they require extar processing to separate out the measurements for the individual parameters.\n", - "\n", - "We skip the other tables in this exercise for either they have very little or fragmented data or they seem uninteresting for the purpose of predicting ozone levels. You can take this as a separate exercise to train a linear regression model by including those parameters. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's maintain an array of dtaframes, one for each parameter, and eventually combine them into a single dataframe." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "params_dfs = []" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's process the tables with single parameter measurements first." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "EDAaIwHpQCDZ" - }, - "outputs": [], - "source": [ - "table_param_dict = {\n", - " \"co_hourly_summary\" : \"co\",\n", - " \"no2_hourly_summary\" : \"no2\",\n", - " \"o3_hourly_summary\" : \"o3\",\n", - " \"pressure_hourly_summary\" : \"pressure\",\n", - " \"so2_hourly_summary\" : \"so2\",\n", - " \"temperature_hourly_summary\" : \"temperature\",\n", - "}\n", - "\n", - "for table, param in table_param_dict.items():\n", - " param_df = bpd.read_gbq(\n", - " f\"{dataset}.{table}\",\n", - " columns=index_columns + [value_column]\n", - " )\n", - " param_df = param_df\\\n", - " .sort_values(index_columns)\\\n", - " .drop_duplicates(index_columns)\\\n", - " .set_index(index_columns)\\\n", - " .rename(columns={value_column : param})\n", - " params_dfs.append(param_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The nonoxnoy table captures measurements for 3 parameters. Let's analyze how many instances of each parameter it contains." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nonoxnoy_table = f\"{dataset}.nonoxnoy_hourly_summary\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bpd.read_gbq(nonoxnoy_table, columns=[param_column]).value_counts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We see that the NOy data is significantly sparse as compared to NO and NOx, so we skip that and include NO and NOx data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "no_df = bpd.read_gbq(\n", - " nonoxnoy_table,\n", - " columns=index_columns + [value_column],\n", - " filters=[(param_column, \"==\", \"Nitric oxide (NO)\")]\n", - ")\n", - "no_df = no_df\\\n", - " .sort_values(index_columns)\\\n", - " .drop_duplicates(index_columns)\\\n", - " .set_index(index_columns)\\\n", - " .rename(columns={value_column: \"no_\"})\n", - "params_dfs.append(no_df)\n", - "\n", - "nox_df = bpd.read_gbq(\n", - " nonoxnoy_table,\n", - " columns=index_columns + [value_column],\n", - " filters=[(param_column, \"==\", \"Oxides of nitrogen (NOx)\")]\n", - ")\n", - "nox_df = nox_df\\\n", - " .sort_values(index_columns)\\\n", - " .drop_duplicates(index_columns)\\\n", - " .set_index(index_columns)\\\n", - " .rename(columns={value_column: \"nox\"})\n", - "params_dfs.append(nox_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The wind table captures measurements for 2 parameters. Let's analyze how many instances of each parameter it contains." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "wind_table = f\"{dataset}.wind_hourly_summary\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bpd.read_gbq(wind_table, columns=[param_column]).value_counts()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's include the data for wind speed and wind direction." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "wind_speed_df = bpd.read_gbq(\n", - " wind_table,\n", - " columns=index_columns + [value_column],\n", - " filters=[(param_column, \"==\", \"Wind Speed - Resultant\")]\n", - ")\n", - "wind_speed_df = wind_speed_df\\\n", - " .sort_values(index_columns)\\\n", - " .drop_duplicates(index_columns)\\\n", - " .set_index(index_columns)\\\n", - " .rename(columns={value_column: \"wind_speed\"})\n", - "params_dfs.append(wind_speed_df)\n", - "\n", - "wind_dir_df = bpd.read_gbq(\n", - " wind_table,\n", - " columns=index_columns + [value_column],\n", - " filters=[(param_column, \"==\", \"Wind Direction - Resultant\")]\n", - ")\n", - "wind_dir_df = wind_dir_df\\\n", - " .sort_values(index_columns)\\\n", - " .drop_duplicates(index_columns)\\\n", - " .set_index(index_columns)\\\n", - " .rename(columns={value_column: \"wind_dir\"})\n", - "params_dfs.append(wind_dir_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's observe each individual parameter and number of data points for each parameter." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for param_df in params_dfs:\n", - " print(f\"{param_df.columns.values}: {len(param_df)}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's combine data from all parameters into a single DataFrame. The measurements for each parameter may not be available for every (state, county, site, date, time) identifier, we will consider only those identifiers for which measurements of all parameters are available. To achieve this we will combine the measurements via \"inner\" join.\n", - "\n", - "We will also materialize this combined data via `cache` method for efficient reuse in the subsequent steps." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df = bpd.concat(params_dfs, axis=1, join=\"inner\").cache()\n", - "df.shape" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rwPLjqW2Ajzh" - }, - "source": [ - "## Clean and prepare data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's temporarily bring the index columns as dataframe columns for further processing on the index values for the purpose of data preparation.\n", - "We will reconstruct the index back at the time of the model training." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df = df.reset_index()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Observe the years from which we have consolidated data so far." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df[\"date_local\"].dt.year.value_counts().sort_index().to_pandas()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this tutorial we would train a model from the past data to predict ozone levels for the future data. Let's define the cut-off year as 2020. We will pretend that the data before 2020 has known ozone levels, and the 2020 onwards the ozone levels are unknown, which we will predict using our model.\n", - "\n", - "We should further separate the known data into training and test sets. The model would be trained on the training set and then evaluated on the test set to make sure the model generalizes beyond the training data. We could use [train_test_split](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.model_selection#bigframes_ml_model_selection_train_test_split) method to randomly split the training and test data, but we leave that for you to try out. In this exercise, let's split based on another cutoff year 2017 - the known data before 2017 would be training data and 2017 onwards would be the test data. This way we stay with the idea that the model is trained on past data and then used to predict the future values." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "6i6HkFJZa8na" - }, - "outputs": [], - "source": [ - "train_data_filter = (df.date_local.dt.year < 2017)\n", - "test_data_filter = (df.date_local.dt.year >= 2017) & (df.date_local.dt.year < 2020)\n", - "predict_data_filter = (df.date_local.dt.year >= 2020)\n", - "\n", - "df_train = df[train_data_filter].set_index(index_columns)\n", - "df_test = df[test_data_filter].set_index(index_columns)\n", - "df_predict = df[predict_data_filter].set_index(index_columns)\n", - "\n", - "df_train.shape, df_test.shape, df_predict.shape" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "M_-0X7NxYK5f" - }, - "source": [ - "Prepare your feature (or input) columns and the target (or output) column for the purpose of model training and evaluation:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "YKwCW7Nsavap" - }, - "outputs": [], - "source": [ - "X_train = df_train.drop(columns=\"o3\")\n", - "y_train = df_train[\"o3\"]\n", - "\n", - "X_test = df_test.drop(columns=\"o3\")\n", - "y_test = df_test[\"o3\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Prepare the unknown data for prediction." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "wej78IDUaRW9" - }, - "outputs": [], - "source": [ - "X_predict = df_predict.drop(columns=\"o3\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Fx4lsNqMorJ-" - }, - "source": [ - "## Create the linear regression model\n", - "\n", - "BigQuery DataFrames ML lets you seamlessly transition from exploring data to creating machine learning models through its scikit-learn-like API, `bigframes.ml`. BigQuery DataFrames ML supports several types of [ML models](https://cloud.google.com/python/docs/reference/bigframes/latest#ml-capabilities).\n", - "\n", - "In this notebook, you create a [`LinearRegression`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.linear_model.LinearRegression) model, a type of regression model that generates a continuous value from a linear combination of input features.\n", - "\n", - "When you create a model with BigQuery DataFrames ML, it is saved in an internal location and limited to the BigQuery DataFrames session. However, as you'll see in the next section, you can use `to_gbq` to save the model permanently to your BigQuery project." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "EloGtMnverFF" - }, - "source": [ - "### Create the model using `bigframes.ml`\n", - "\n", - "Please note that BigQuery DataFrames ML is backed by BigQuery ML, which uses\n", - "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values when you pass the feature columns without transforms.\n", - "\n", - "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "GskyyUQPowBT" - }, - "outputs": [], - "source": [ - "from bigframes.ml.linear_model import LinearRegression\n", - "\n", - "model = LinearRegression()\n", - "\n", - "model.fit(X_train, y_train)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "UGjeMPC2caKK" - }, - "source": [ - "### Score the model\n", - "\n", - "Check how the model performs by using the [`score`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.linear_model.LinearRegression#bigframes_ml_linear_model_LinearRegression_score) method. More information on BigQuery ML model scoring can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "kGBJKafpo0dl" - }, - "outputs": [], - "source": [ - "# On the training data\n", - "model.score(X_train, y_train)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# On the test data\n", - "model.score(X_test, y_test)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "P2lUiZZ_cjri" - }, - "source": [ - "### Predict using the model\n", - "\n", - "Use the model to predict the levels of ozone. The predicted levels are returned in the column `predicted_o3`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bsQ9cmoWo0Ps" - }, - "outputs": [], - "source": [ - "df_pred = model.predict(X_predict)\n", - "df_pred.peek()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GTRdUw-Ro5R1" - }, - "source": [ - "## Save the model in BigQuery\n", - "\n", - "The model is saved locally within this session. You can save the model permanently to BigQuery for use in future sessions, and to make the model sharable with others." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "K0mPaoGpcwwy" - }, - "source": [ - "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ZSP7gt13QrQt" - }, - "outputs": [], - "source": [ - "DATASET_ID = \"\" # @param {type:\"string\"}\n", - "\n", - "if not DATASET_ID:\n", - " raise ValueError(\"Please define the DATASET_ID\")\n", - "\n", - "client = bpd.get_global_session().bqclient\n", - "dataset = client.create_dataset(DATASET_ID, exists_ok=True)\n", - "print(f\"Dataset {dataset.dataset_id} created.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zqAIWWgJczp-" - }, - "source": [ - "Save the model using the `to_gbq` method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "QE_GD4Byo_jb" - }, - "outputs": [], - "source": [ - "model.to_gbq(DATASET_ID + \".o3_lr_model\" , replace=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f7uHacAy49rT" - }, - "source": [ - "You can view the saved model in the BigQuery console under the dataset you created in the first step. Run the following cell and follow the link to view your BigQuery console:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "qDBoiA_0488Z" - }, - "outputs": [], - "source": [ - "print(f'https://console.cloud.google.com/bigquery?ws=!1m5!1m4!5m3!1s{PROJECT_ID}!2s{DATASET_ID}!3so3_lr_model')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "G_wjSfXpWTuy" - }, - "source": [ - "# Summary and next steps\n", - "\n", - "You've created a linear regression model using `bigframes.ml`.\n", - "\n", - "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TpV-iwP9qw9c" - }, - "source": [ - "## Cleaning up\n", - "\n", - "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", - "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", - "\n", - "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "sx_vKniMq9ZX" - }, - "outputs": [], - "source": [ - "# # Delete the BigQuery dataset and associated ML model\n", - "# client.delete_dataset(DATASET_ID, delete_contents=True, not_found_ok=True)" - ] - } - ], - "metadata": { - "colab": { - "provenance": [], - "toc_visible": true - }, - "kernelspec": { - "display_name": "Python 3", - "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.0" - } - }, - "nbformat": 4, - "nbformat_minor": 0 + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "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": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Train a linear regression model with BigQuery DataFrames ML", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.11" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates training a linear regression model on Big Data using BigQuery DataFrames ML. BigQuery DataFrames ML provides a provides a scikit-learn-like API for ML powered by the BigQuery engine.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/python/docs/reference/bigframes/latest)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, we use BigQuery DataFrames to create a linear regression model that predicts the levels of Ozone in the atmosphere.\n", + "\n", + "The steps include:\n", + "\n", + "- Creating a DataFrame from the BigQuery table.\n", + "- Cleaning and preparing data using `bigframes.pandas` module.\n", + "- Creating a linear regression model using `bigframes.ml` module.\n", + "- Saving the ML model to BigQuery for future use.\n", + "\n", + "\n", + "Let's formally define our problem as: **Train a linear regression model to predict the level of ozone in the atmosphere given the measurements of other constituents and properties of the atmosphere.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "In this tutorial we are going to use the [`bigquery-public-data.epa_historical_air_quality`](https://console.cloud.google.com/marketplace/product/epa/historical-air-quality) dataset. To quote the description of the dataset:\n", + "\n", + "\"The United States Environmental Protection Agency (EPA) protects both public health and the environment by establishing the standards for national air quality. The EPA provides annual summary data as well as hourly and daily data in the categories of criteria gases, particulates, meteorological, and toxics.\"\n", + "\n", + "There are several tables capturing data about the constituents of the atmosphere, see them in the [BigQuery cloud console](https://pantheon.corp.google.com/bigquery?p=bigquery-public-data&d=epa_historical_air_quality&page=dataset). Most tables carry 10's of GBs of data, but that is not an issue with BigQuery DataFrames as the data is efficiently processed at BigQuery without transferring them to the client." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models)\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "If you don't have [bigframes](https://pypi.org/project/bigframes/) package already installed, uncomment and execute the following cells to\n", + "\n", + "1. Install the package\n", + "1. Restart the notebook kernel (Jupyter or Colab) to work with the package" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9O0Ka4W2MNF3" + }, + "outputs": [], + "source": [ + "# !pip install bigframes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f200f10a1da3" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "\n", + "# import IPython\n", + "#\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "Complete the tasks in this section to set up your environment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oDfTjfACBvJk" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "If you don't know your project ID, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "region" + }, + "source": [ + "#### Set the BigQuery location\n", + "\n", + "You can also change the `LOCATION` variable used by BigQuery. Learn more about [BigQuery locations](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eF-Twtc4XGem" + }, + "outputs": [], + "source": [ + "LOCATION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Set up APIs, IAM permissions and Authentication\n", + "\n", + "Follow the instructions at https://cloud.google.com/bigquery/docs/use-bigquery-dataframes#permissions.\n", + "\n", + "Depending on your notebook environment, you might have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**Vertex AI Workbench**\n", + "\n", + "Do nothing, you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**Local JupyterLab instance**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login\n", + "# ! gcloud auth application-default login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**Colab**\n", + "\n", + "Uncomment and run the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Set BigQuery DataFrames options" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NPPMuw2PXGeo" + }, + "outputs": [], + "source": [ + "# NOTE: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# NOTE: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = LOCATION\n", + "\n", + "# NOTE: For a machine learning model the order of the data is\n", + "# not important. So let's relax the ordering_mode to accept\n", + "# partial ordering. This allows BigQuery DataFrames to run cost\n", + "# and performance optimized jobs at the BigQuery engine.\n", + "bpd.options.bigquery.ordering_mode = \"partial\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D21CoOlfFTYI" + }, + "source": [ + "If you want to reset the location of the created DataFrame or Series objects, reset the session by executing `bpd.close_session()`. After that, you can reuse `bpd.options.bigquery.location` to specify another location." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9EMAqR37AfLS" + }, + "source": [ + "## Read data in BigQuery tables as DataFrame\n", + "\n", + "Let's read the tables in the dataset to construct a BigQuery DataFrames DataFrame. We will combine measurements of various parameters of the atmosphere from multiple tables to represent a consolidated dataframe to use for our model training and prediction. We have daily and hourly versions of the data available, but since we want to create a model that is dynamic so that it can capture the variance throughout the day, we would choose the hourly version.\n", + "\n", + "Note that we would use the pandas APIs as we normally would on the BigQuery DataFrames DataFrame, but calculations happen in the BigQuery query engine instead of the local environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = \"bigquery-public-data.epa_historical_air_quality\"\n", + "hourly_summary_tables = [\n", + " \"co_hourly_summary\",\n", + " \"hap_hourly_summary\",\n", + " \"no2_hourly_summary\",\n", + " \"nonoxnoy_hourly_summary\",\n", + " \"o3_hourly_summary\",\n", + " \"pm10_hourly_summary\",\n", + " \"pm25_frm_hourly_summary\",\n", + " \"pm25_nonfrm_hourly_summary\",\n", + " \"pm25_speciation_hourly_summary\",\n", + " \"pressure_hourly_summary\",\n", + " \"rh_and_dp_hourly_summary\",\n", + " \"so2_hourly_summary\",\n", + " \"temperature_hourly_summary\",\n", + " \"voc_hourly_summary\",\n", + " \"wind_hourly_summary\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's pick index columns - to identify a measurement of the atmospheric parameter, param column - to identify which param the measurement pertains to, and value column - the column containing the measurement itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "index_columns = [\"state_name\", \"county_name\", \"site_num\", \"date_local\", \"time_local\"]\n", + "param_column = \"parameter_name\"\n", + "value_column = \"sample_measurement\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's observe how much data each table contains:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for table in hourly_summary_tables:\n", + " # get the bigframes global session\n", + " bigframes_session = bpd.get_global_session()\n", + "\n", + " # get the bigquery table info\n", + " table_info = bigframes_session.bqclient.get_table(f\"{dataset}.{table}\")\n", + "\n", + " # read the table as a dataframe\n", + " df = bpd.read_gbq(f\"{dataset}.{table}\")\n", + "\n", + " # print metadata about the table\n", + " print(\n", + " f\"{table}: \"\n", + " f\"{round(table_info.num_bytes/1_000_000_000, 1)} GB, \"\n", + " f\"{round(table_info.num_rows/1_000_000, 1)} million rows, \"\n", + " f\"{df[param_column].nunique()} params\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's be mindful that the rows in each table may contain duplicates, which may introdude bias in any model trained on the raw data. We will make sure to drop the duplicates when we use the data for model training.\n", + "\n", + "Since we want to predict ozone level, we obviously pick the `o3` table. Let's also pick the tables about other gases - `co`, `no2` and `so2`. Let's also pick `pressure` and `temperature` tables as they seem fundamental indicators for the atmosphere. Note that each of these tables capture measurements for a single parameter (i.e. the column `parameter_name` has a single unique value).\n", + "\n", + "We are also interested in the nonoxny and wind tables, but they capture multiple parameters (i.e. the column `parameter_name` has a more than one unique values). We will include their measurements in later step, as they require extar processing to separate out the measurements for the individual parameters.\n", + "\n", + "We skip the other tables in this exercise for either they have very little or fragmented data or they seem uninteresting for the purpose of predicting ozone levels. You can take this as a separate exercise to train a linear regression model by including those parameters. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's maintain an array of dtaframes, one for each parameter, and eventually combine them into a single dataframe." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "params_dfs = []" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's process the tables with single parameter measurements first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EDAaIwHpQCDZ" + }, + "outputs": [], + "source": [ + "table_param_dict = {\n", + " \"co_hourly_summary\" : \"co\",\n", + " \"no2_hourly_summary\" : \"no2\",\n", + " \"o3_hourly_summary\" : \"o3\",\n", + " \"pressure_hourly_summary\" : \"pressure\",\n", + " \"so2_hourly_summary\" : \"so2\",\n", + " \"temperature_hourly_summary\" : \"temperature\",\n", + "}\n", + "\n", + "for table, param in table_param_dict.items():\n", + " param_df = bpd.read_gbq(\n", + " f\"{dataset}.{table}\",\n", + " columns=index_columns + [value_column]\n", + " )\n", + " param_df = param_df\\\n", + " .sort_values(index_columns)\\\n", + " .drop_duplicates(index_columns)\\\n", + " .set_index(index_columns)\\\n", + " .rename(columns={value_column : param})\n", + " params_dfs.append(param_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The nonoxnoy table captures measurements for 3 parameters. Let's analyze how many instances of each parameter it contains." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nonoxnoy_table = f\"{dataset}.nonoxnoy_hourly_summary\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bpd.read_gbq(nonoxnoy_table, columns=[param_column]).value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that the NOy data is significantly sparse as compared to NO and NOx, so we skip that and include NO and NOx data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "no_df = bpd.read_gbq(\n", + " nonoxnoy_table,\n", + " columns=index_columns + [value_column],\n", + " filters=[(param_column, \"==\", \"Nitric oxide (NO)\")]\n", + ")\n", + "no_df = no_df\\\n", + " .sort_values(index_columns)\\\n", + " .drop_duplicates(index_columns)\\\n", + " .set_index(index_columns)\\\n", + " .rename(columns={value_column: \"no_\"})\n", + "params_dfs.append(no_df)\n", + "\n", + "nox_df = bpd.read_gbq(\n", + " nonoxnoy_table,\n", + " columns=index_columns + [value_column],\n", + " filters=[(param_column, \"==\", \"Oxides of nitrogen (NOx)\")]\n", + ")\n", + "nox_df = nox_df\\\n", + " .sort_values(index_columns)\\\n", + " .drop_duplicates(index_columns)\\\n", + " .set_index(index_columns)\\\n", + " .rename(columns={value_column: \"nox\"})\n", + "params_dfs.append(nox_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The wind table captures measurements for 2 parameters. Let's analyze how many instances of each parameter it contains." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "wind_table = f\"{dataset}.wind_hourly_summary\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bpd.read_gbq(wind_table, columns=[param_column]).value_counts()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's include the data for wind speed and wind direction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "wind_speed_df = bpd.read_gbq(\n", + " wind_table,\n", + " columns=index_columns + [value_column],\n", + " filters=[(param_column, \"==\", \"Wind Speed - Resultant\")]\n", + ")\n", + "wind_speed_df = wind_speed_df\\\n", + " .sort_values(index_columns)\\\n", + " .drop_duplicates(index_columns)\\\n", + " .set_index(index_columns)\\\n", + " .rename(columns={value_column: \"wind_speed\"})\n", + "params_dfs.append(wind_speed_df)\n", + "\n", + "wind_dir_df = bpd.read_gbq(\n", + " wind_table,\n", + " columns=index_columns + [value_column],\n", + " filters=[(param_column, \"==\", \"Wind Direction - Resultant\")]\n", + ")\n", + "wind_dir_df = wind_dir_df\\\n", + " .sort_values(index_columns)\\\n", + " .drop_duplicates(index_columns)\\\n", + " .set_index(index_columns)\\\n", + " .rename(columns={value_column: \"wind_dir\"})\n", + "params_dfs.append(wind_dir_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's observe each individual parameter and number of data points for each parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for param_df in params_dfs:\n", + " print(f\"{param_df.columns.values}: {len(param_df)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's combine data from all parameters into a single DataFrame. The measurements for each parameter may not be available for every (state, county, site, date, time) identifier, we will consider only those identifiers for which measurements of all parameters are available. To achieve this we will combine the measurements via \"inner\" join.\n", + "\n", + "We will also materialize this combined data via `cache` method for efficient reuse in the subsequent steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = bpd.concat(params_dfs, axis=1, join=\"inner\").cache()\n", + "df.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rwPLjqW2Ajzh" + }, + "source": [ + "## Clean and prepare data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's temporarily bring the index columns as dataframe columns for further processing on the index values for the purpose of data preparation.\n", + "We will reconstruct the index back at the time of the model training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = df.reset_index()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Observe the years from which we have consolidated data so far." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df[\"date_local\"].dt.year.value_counts().sort_index().to_pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial we would train a model from the past data to predict ozone levels for the future data. Let's define the cut-off year as 2020. We will pretend that the data before 2020 has known ozone levels, and the 2020 onwards the ozone levels are unknown, which we will predict using our model.\n", + "\n", + "We should further separate the known data into training and test sets. The model would be trained on the training set and then evaluated on the test set to make sure the model generalizes beyond the training data. We could use [train_test_split](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.model_selection#bigframes_ml_model_selection_train_test_split) method to randomly split the training and test data, but we leave that for you to try out. In this exercise, let's split based on another cutoff year 2017 - the known data before 2017 would be training data and 2017 onwards would be the test data. This way we stay with the idea that the model is trained on past data and then used to predict the future values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6i6HkFJZa8na" + }, + "outputs": [], + "source": [ + "train_data_filter = (df.date_local.dt.year < 2017)\n", + "test_data_filter = (df.date_local.dt.year >= 2017) & (df.date_local.dt.year < 2020)\n", + "predict_data_filter = (df.date_local.dt.year >= 2020)\n", + "\n", + "df_train = df[train_data_filter].set_index(index_columns)\n", + "df_test = df[test_data_filter].set_index(index_columns)\n", + "df_predict = df[predict_data_filter].set_index(index_columns)\n", + "\n", + "df_train.shape, df_test.shape, df_predict.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M_-0X7NxYK5f" + }, + "source": [ + "Prepare your feature (or input) columns and the target (or output) column for the purpose of model training and evaluation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YKwCW7Nsavap" + }, + "outputs": [], + "source": [ + "X_train = df_train.drop(columns=\"o3\")\n", + "y_train = df_train[\"o3\"]\n", + "\n", + "X_test = df_test.drop(columns=\"o3\")\n", + "y_test = df_test[\"o3\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Prepare the unknown data for prediction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wej78IDUaRW9" + }, + "outputs": [], + "source": [ + "X_predict = df_predict.drop(columns=\"o3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fx4lsNqMorJ-" + }, + "source": [ + "## Create the linear regression model\n", + "\n", + "BigQuery DataFrames ML lets you seamlessly transition from exploring data to creating machine learning models through its scikit-learn-like API, `bigframes.ml`. BigQuery DataFrames ML supports several types of [ML models](https://cloud.google.com/python/docs/reference/bigframes/latest#ml-capabilities).\n", + "\n", + "In this notebook, you create a [`LinearRegression`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.linear_model.LinearRegression) model, a type of regression model that generates a continuous value from a linear combination of input features.\n", + "\n", + "When you create a model with BigQuery DataFrames ML, it is saved in an internal location and limited to the BigQuery DataFrames session. However, as you'll see in the next section, you can use `to_gbq` to save the model permanently to your BigQuery project." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EloGtMnverFF" + }, + "source": [ + "### Create the model using `bigframes.ml`\n", + "\n", + "Please note that BigQuery DataFrames ML is backed by BigQuery ML, which uses\n", + "[automatic preprocessing](https://cloud.google.com/bigquery/docs/auto-preprocessing) to encode string values and scale numeric values when you pass the feature columns without transforms.\n", + "\n", + "BigQuery ML also [automatically splits the data for training and evaluation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-glm#data_split_method), although for datasets with less than 500 rows (such as this one), all rows are used for training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GskyyUQPowBT" + }, + "outputs": [], + "source": [ + "from bigframes.ml.linear_model import LinearRegression\n", + "\n", + "model = LinearRegression()\n", + "\n", + "model.fit(X_train, y_train)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UGjeMPC2caKK" + }, + "source": [ + "### Score the model\n", + "\n", + "Check how the model performs by using the [`score`](https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.linear_model.LinearRegression#bigframes_ml_linear_model_LinearRegression_score) method. More information on BigQuery ML model scoring can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate#mlevaluate_output)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kGBJKafpo0dl" + }, + "outputs": [], + "source": [ + "# On the training data\n", + "model.score(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# On the test data\n", + "model.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P2lUiZZ_cjri" + }, + "source": [ + "### Predict using the model\n", + "\n", + "Use the model to predict the levels of ozone. The predicted levels are returned in the column `predicted_o3`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bsQ9cmoWo0Ps" + }, + "outputs": [], + "source": [ + "df_pred = model.predict(X_predict)\n", + "df_pred.peek()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GTRdUw-Ro5R1" + }, + "source": [ + "## Save the model in BigQuery\n", + "\n", + "The model is saved locally within this session. You can save the model permanently to BigQuery for use in future sessions, and to make the model sharable with others." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K0mPaoGpcwwy" + }, + "source": [ + "Create a BigQuery dataset to house the model, adding a name for your dataset as the `DATASET_ID` variable:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZSP7gt13QrQt" + }, + "outputs": [], + "source": [ + "DATASET_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "if not DATASET_ID:\n", + " raise ValueError(\"Please define the DATASET_ID\")\n", + "\n", + "client = bpd.get_global_session().bqclient\n", + "dataset = client.create_dataset(DATASET_ID, exists_ok=True)\n", + "print(f\"Dataset {dataset.dataset_id} created.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zqAIWWgJczp-" + }, + "source": [ + "Save the model using the `to_gbq` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QE_GD4Byo_jb" + }, + "outputs": [], + "source": [ + "model.to_gbq(DATASET_ID + \".o3_lr_model\" , replace=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f7uHacAy49rT" + }, + "source": [ + "You can view the saved model in the BigQuery console under the dataset you created in the first step. Run the following cell and follow the link to view your BigQuery console:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qDBoiA_0488Z" + }, + "outputs": [], + "source": [ + "print(f'https://console.cloud.google.com/bigquery?ws=!1m5!1m4!5m3!1s{PROJECT_ID}!2s{DATASET_ID}!3so3_lr_model')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G_wjSfXpWTuy" + }, + "source": [ + "# Summary and next steps\n", + "\n", + "You've created a linear regression model using `bigframes.ml`.\n", + "\n", + "Learn more about BigQuery DataFrames in the [documentation](https://cloud.google.com/python/docs/reference/bigframes/latest) and find more sample notebooks in the [GitHub repo](https://github.com/googleapis/python-bigquery-dataframes/tree/main/notebooks)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sx_vKniMq9ZX" + }, + "outputs": [], + "source": [ + "# # Delete the BigQuery dataset and associated ML model\n", + "# client.delete_dataset(DATASET_ID, delete_contents=True, not_found_ok=True)" + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/ml/timeseries_analysis.ipynb b/notebooks/ml/timeseries_analysis.ipynb index 84959b3632b..3b227460230 100644 --- a/notebooks/ml/timeseries_analysis.ipynb +++ b/notebooks/ml/timeseries_analysis.ipynb @@ -27,7 +27,7 @@ "id": "0eba46b9", "metadata": {}, "source": [ - "### 1. Data Loading and Preprocessing\n", + "## 1. Data Loading and Preprocessing", "\n", "The first step is to load the San Francisco bikeshare dataset from BigQuery. We then preprocess the data by filtering for trips made by 'Subscriber' type users from 2018 onwards. This ensures we are working with a relevant and consistent subset of the data. Finally, we aggregate the trip data by the hour to create a time series of trip counts." ] diff --git a/notebooks/multimodal/multimodal_dataframe.ipynb b/notebooks/multimodal/multimodal_dataframe.ipynb index 89af5767113..1d3945c92c3 100644 --- a/notebooks/multimodal/multimodal_dataframe.ipynb +++ b/notebooks/multimodal/multimodal_dataframe.ipynb @@ -1,1523 +1,1523 @@ { - "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": { - "id": "YOrUAvz6DMw-" - }, - "source": [ - "# BigFrames Multimodal DataFrame\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook is introducing BigFrames Multimodal features:\n", - "1. Create Multimodal DataFrame\n", - "2. Combine unstructured data with structured data\n", - "3. Conduct image transformations\n", - "4. Use LLM models to ask questions and generate embeddings on images\n", - "5. PDF chunking function\n", - "6. Transcribe audio\n", - "7. Extract EXIF metadata from images" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PEAJQQ6AFg-n" - }, - "source": [ - "### Setup" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Install the latest bigframes package if bigframes version < 2.4.0" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install bigframes --upgrade" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "bGyhLnfEeB0X", - "outputId": "83ac8b64-3f44-4d43-d089-28a5026cbb42" - }, - "outputs": [], - "source": [ - "PROJECT = \"bigframes-dev\" # replace with your project. \n", - "# Refer to https://cloud.google.com/bigquery/docs/multimodal-data-dataframes-tutorial#required_roles for your required permissions\n", - "\n", - "LOCATION = \"us\" # replace with your location.\n", - "\n", - "# Dataset where the UDF will be created.\n", - "DATASET_ID = \"bigframes_samples\" # replace with your dataset ID.\n", - "\n", - "OUTPUT_BUCKET = \"bigframes_blob_test\" # replace with your GCS bucket. \n", - "# The connection (or bigframes-default-connection of the project) must have read/write permission to the bucket. \n", - "# Refer to https://cloud.google.com/bigquery/docs/multimodal-data-dataframes-tutorial#grant-permissions for setting up connection service account permissions.\n", - "# In this Notebook it uses bigframes-default-connection by default. You can also bring in your own connections in each method.\n", - "\n", - "import bigframes\n", - "# Setup project\n", - "bigframes.options.bigquery.project = PROJECT\n", - "bigframes.options.bigquery.location = LOCATION\n", - "\n", - "# Display options\n", - "bigframes.options.display.blob_display_width = 300\n", - "bigframes.options.display.progress_bar = None\n", - "\n", - "import bigframes.pandas as bpd\n", - "import bigframes.bigquery as bbq" - ] + "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": { + "id": "YOrUAvz6DMw-" + }, + "source": [ + "# BigFrames Multimodal DataFrame\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is introducing BigFrames Multimodal features:\n", + "1. Create Multimodal DataFrame\n", + "2. Combine unstructured data with structured data\n", + "3. Conduct image transformations\n", + "4. Use LLM models to ask questions and generate embeddings on images\n", + "5. PDF chunking function\n", + "6. Transcribe audio\n", + "7. Extract EXIF metadata from images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PEAJQQ6AFg-n" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install the latest bigframes package if bigframes version < 2.4.0" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install bigframes --upgrade" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import bigframes.bigquery as bbq\n", - "\n", - "def get_runtime_json_str(series, mode=\"R\", with_metadata=False):\n", - " \"\"\"\n", - " Get the runtime (contains signed URL to access gcs data) and apply the\n", - " ToJSONSTring transformation.\n", - " \n", - " Args:\n", - " series: bigframes.series.Series to operate on.\n", - " mode: \"R\" for read, \"RW\" for read/write.\n", - " with_metadata: Whether to fetch and include blob metadata.\n", - " \"\"\"\n", - " # 1. Optionally fetch metadata\n", - " s = (\n", - " bbq.obj.fetch_metadata(series)\n", - " if with_metadata\n", - " else series\n", - " )\n", - " \n", - " # 2. Retrieve the access URL runtime object\n", - " runtime = bbq.obj.get_access_url(s, mode=mode)\n", - " \n", - " # 3. Convert the runtime object to a JSON string\n", - " return bbq.to_json_string(runtime)\n", - "\n", - "def get_metadata(series):\n", - " # Fetch metadata and extract GCS metadata from the details JSON field\n", - " metadata_obj = bbq.obj.fetch_metadata(series)\n", - " return bbq.json_query(metadata_obj.struct.field(\"details\"), \"$.gcs_metadata\")\n", - "\n", - "def get_content_type(series):\n", - " return bbq.json_value(get_metadata(series), \"$.content_type\")\n", - "\n", - "def get_size(series):\n", - " return bbq.json_value(get_metadata(series), \"$.size\").astype(\"Int64\")\n", - "\n", - "def get_updated(series):\n", - " return bpd.to_datetime(bbq.json_value(get_metadata(series), \"$.updated\").astype(\"Int64\"), unit=\"us\", utc=True)" - ] + "id": "bGyhLnfEeB0X", + "outputId": "83ac8b64-3f44-4d43-d089-28a5026cbb42" + }, + "outputs": [], + "source": [ + "PROJECT = \"bigframes-dev\" # replace with your project. \n", + "# Refer to https://cloud.google.com/bigquery/docs/multimodal-data-dataframes-tutorial#required_roles for your required permissions\n", + "\n", + "LOCATION = \"us\" # replace with your location.\n", + "\n", + "# Dataset where the UDF will be created.\n", + "DATASET_ID = \"bigframes_samples\" # replace with your dataset ID.\n", + "\n", + "OUTPUT_BUCKET = \"bigframes_blob_test\" # replace with your GCS bucket. \n", + "# The connection (or bigframes-default-connection of the project) must have read/write permission to the bucket. \n", + "# Refer to https://cloud.google.com/bigquery/docs/multimodal-data-dataframes-tutorial#grant-permissions for setting up connection service account permissions.\n", + "# In this Notebook it uses bigframes-default-connection by default. You can also bring in your own connections in each method.\n", + "\n", + "import bigframes\n", + "# Setup project\n", + "bigframes.options.bigquery.project = PROJECT\n", + "bigframes.options.bigquery.location = LOCATION\n", + "\n", + "# Display options\n", + "bigframes.options.display.blob_display_width = 300\n", + "bigframes.options.display.progress_bar = None\n", + "\n", + "import bigframes.pandas as bpd\n", + "import bigframes.bigquery as bbq" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import bigframes.bigquery as bbq\n", + "\n", + "def get_runtime_json_str(series, mode=\"R\", with_metadata=False):\n", + " \"\"\"\n", + " Get the runtime (contains signed URL to access gcs data) and apply the\n", + " ToJSONSTring transformation.\n", + " \n", + " Args:\n", + " series: bigframes.series.Series to operate on.\n", + " mode: \"R\" for read, \"RW\" for read/write.\n", + " with_metadata: Whether to fetch and include blob metadata.\n", + " \"\"\"\n", + " # 1. Optionally fetch metadata\n", + " s = (\n", + " bbq.obj.fetch_metadata(series)\n", + " if with_metadata\n", + " else series\n", + " )\n", + " \n", + " # 2. Retrieve the access URL runtime object\n", + " runtime = bbq.obj.get_access_url(s, mode=mode)\n", + " \n", + " # 3. Convert the runtime object to a JSON string\n", + " return bbq.to_json_string(runtime)\n", + "\n", + "def get_metadata(series):\n", + " # Fetch metadata and extract GCS metadata from the details JSON field\n", + " metadata_obj = bbq.obj.fetch_metadata(series)\n", + " return bbq.json_query(metadata_obj.struct.field(\"details\"), \"$.gcs_metadata\")\n", + "\n", + "def get_content_type(series):\n", + " return bbq.json_value(get_metadata(series), \"$.content_type\")\n", + "\n", + "def get_size(series):\n", + " return bbq.json_value(get_metadata(series), \"$.size\").astype(\"Int64\")\n", + "\n", + "def get_updated(series):\n", + " return bpd.to_datetime(bbq.json_value(get_metadata(series), \"$.updated\").astype(\"Int64\"), unit=\"us\", utc=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ifKOq7VZGtZy" + }, + "source": [ + "### 1. Create Multimodal DataFrame\n", + "There are several ways to create Multimodal DataFrame. The easiest way is from the wildcard paths." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, - { - "cell_type": "markdown", - "metadata": { - "id": "ifKOq7VZGtZy" - }, - "source": [ - "### 1. Create Multimodal DataFrame\n", - "There are several ways to create Multimodal DataFrame. The easiest way is from the wildcard paths." - ] + "id": "fx6YcZJbeYru", + "outputId": "d707954a-0dd0-4c50-b7bf-36b140cf76cf" + }, + "outputs": [], + "source": [ + "# Create blob columns from wildcard path.\n", + "df_image = bpd.from_glob_path(\n", + " \"gs://cloud-samples-data/bigquery/tutorials/cymbal-pets/images/*\", name=\"image\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 487 }, + "id": "HhCb8jRsLe9B", + "outputId": "03081cf9-3a22-42c9-b38f-649f592fdada" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "fx6YcZJbeYru", - "outputId": "d707954a-0dd0-4c50-b7bf-36b140cf76cf" - }, - "outputs": [], - "source": [ - "# Create blob columns from wildcard path.\n", - "df_image = bpd.from_glob_path(\n", - " \"gs://cloud-samples-data/bigquery/tutorials/cymbal-pets/images/*\", name=\"image\"\n", - ")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 487 - }, - "id": "HhCb8jRsLe9B", - "outputId": "03081cf9-3a22-42c9-b38f-649f592fdada" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\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", - " \n", - " \n", - " \n", - " \n", - "
image
0
1
2
3
4
\n", - "

5 rows × 1 columns

\n", - "
[5 rows x 1 columns in total]" - ], - "text/plain": [ - " image\n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", - "\n", - "[5 rows x 1 columns]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } + "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", + "
image
0
1
2
3
4
\n", + "

5 rows × 1 columns

\n", + "
[5 rows x 1 columns in total]" ], - "source": [ - "# Take only the 5 images to deal with. Preview the content of the Mutimodal DataFrame\n", - "df_image = df_image.head(5)\n", - "df_image" + "text/plain": [ + " image\n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3...\n", + "\n", + "[5 rows x 1 columns]" ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "b6RRZb3qPi_T" - }, - "source": [ - "### 2. Combine unstructured data with structured data" - ] - }, + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Take only the 5 images to deal with. Preview the content of the Mutimodal DataFrame\n", + "df_image = df_image.head(5)\n", + "df_image" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b6RRZb3qPi_T" + }, + "source": [ + "### 2. Combine unstructured data with structured data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4YJCdmLtR-qu" + }, + "source": [ + "Now you can put more information into the table to describe the files. Such as author info from inputs, or other metadata from the gcs object itself." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "YYYVn7NDH0Me" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "4YJCdmLtR-qu" - }, - "source": [ - "Now you can put more information into the table to describe the files. Such as author info from inputs, or other metadata from the gcs object itself." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "YYYVn7NDH0Me" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\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", - " \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", - "
imageauthorcontent_typesizeupdated
0aliceimage/png15912402025-03-20 17:45:04+00:00
1bobimage/png11829512025-03-20 17:45:02+00:00
2bobimage/png15208842025-03-20 17:44:55+00:00
3aliceimage/png12354012025-03-20 17:45:19+00:00
4bobimage/png15919232025-03-20 17:44:47+00:00
\n", - "

5 rows × 5 columns

\n", - "
[5 rows x 5 columns in total]" - ], - "text/plain": [ - " image author content_type \\\n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... alice image/png \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... alice image/png \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", - "\n", - " size updated \n", - "0 1591240 2025-03-20 17:45:04+00:00 \n", - "1 1182951 2025-03-20 17:45:02+00:00 \n", - "2 1520884 2025-03-20 17:44:55+00:00 \n", - "3 1235401 2025-03-20 17:45:19+00:00 \n", - "4 1591923 2025-03-20 17:44:47+00:00 \n", - "\n", - "[5 rows x 5 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
imageauthorcontent_typesizeupdated
0aliceimage/png15912402025-03-20 17:45:04+00:00
1bobimage/png11829512025-03-20 17:45:02+00:00
2bobimage/png15208842025-03-20 17:44:55+00:00
3aliceimage/png12354012025-03-20 17:45:19+00:00
4bobimage/png15919232025-03-20 17:44:47+00:00
\n", + "

5 rows × 5 columns

\n", + "
[5 rows x 5 columns in total]" ], - "source": [ - "# Combine unstructured data with structured data\n", - "df_image = df_image.head(5)\n", - "df_image[\"author\"] = [\"alice\", \"bob\", \"bob\", \"alice\", \"bob\"] # type: ignore\n", - "df_image[\"content_type\"] = get_content_type(df_image[\"image\"])\n", - "df_image[\"size\"] = get_size(df_image[\"image\"])\n", - "df_image[\"updated\"] = get_updated(df_image[\"image\"])\n", - "df_image" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3. Conduct image transformations" + "text/plain": [ + " image author content_type \\\n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... alice image/png \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... alice image/png \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... bob image/png \n", + "\n", + " size updated \n", + "0 1591240 2025-03-20 17:45:04+00:00 \n", + "1 1182951 2025-03-20 17:45:02+00:00 \n", + "2 1520884 2025-03-20 17:44:55+00:00 \n", + "3 1235401 2025-03-20 17:45:19+00:00 \n", + "4 1591923 2025-03-20 17:44:47+00:00 \n", + "\n", + "[5 rows x 5 columns]" ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Combine unstructured data with structured data\n", + "df_image = df_image.head(5)\n", + "df_image[\"author\"] = [\"alice\", \"bob\", \"bob\", \"alice\", \"bob\"] # type: ignore\n", + "df_image[\"content_type\"] = get_content_type(df_image[\"image\"])\n", + "df_image[\"size\"] = get_size(df_image[\"image\"])\n", + "df_image[\"updated\"] = get_updated(df_image[\"image\"])\n", + "df_image" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Conduct image transformations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section demonstrates how to perform image transformations like blur, resize, and normalize using custom BigQuery Python UDFs and the `opencv-python` library." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 487 }, + "id": "HhCb8jRsLe9B", + "outputId": "03081cf9-3a22-42c9-b38f-649f592fdada" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This section demonstrates how to perform image transformations like blur, resize, and normalize using custom BigQuery Python UDFs and the `opencv-python` library." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", + " return global_session.with_default_session(\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dataframe.py:4655: FunctionAxisOnePreviewWarning: DataFrame.apply with parameter axis=1 scenario is in preview.\n", + " warnings.warn(msg, category=bfe.FunctionAxisOnePreviewWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 487 - }, - "id": "HhCb8jRsLe9B", - "outputId": "03081cf9-3a22-42c9-b38f-649f592fdada" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", - " return global_session.with_default_session(\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dataframe.py:4655: FunctionAxisOnePreviewWarning: DataFrame.apply with parameter axis=1 scenario is in preview.\n", - " warnings.warn(msg, category=bfe.FunctionAxisOnePreviewWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
imageblurred
0
1
2
3
4
\n", - "

5 rows × 2 columns

\n", - "
[5 rows x 2 columns in total]" - ], - "text/plain": [ - " image \\\n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "\n", - " blurred \n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", - "\n", - "[5 rows x 2 columns]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
imageblurred
0
1
2
3
4
\n", + "

5 rows × 2 columns

\n", + "
[5 rows x 2 columns in total]" ], - "source": [ - "# Construct the canonical connection ID\n", - "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", - "\n", - "@bpd.udf(\n", - " input_types=[str, str, int, int],\n", - " output_type=str,\n", - " dataset=DATASET_ID,\n", - " name=\"image_blur\",\n", - " bigquery_connection=FULL_CONNECTION_ID,\n", - " packages=[\"opencv-python\", \"numpy\", \"requests\"],\n", - ")\n", - "def image_blur(src_rt: str, dst_rt: str, kx: int, ky: int) -> str:\n", - " import json\n", - " import cv2 as cv\n", - " import numpy as np\n", - " import requests\n", - " import base64\n", - "\n", - " src_obj = json.loads(src_rt)\n", - " src_url = src_obj[\"access_urls\"][\"read_url\"]\n", - " \n", - " response = requests.get(src_url, timeout=30)\n", - " response.raise_for_status()\n", - " \n", - " img = cv.imdecode(np.frombuffer(response.content, np.uint8), cv.IMREAD_UNCHANGED)\n", - " if img is None:\n", - " raise ValueError(\"cv.imdecode failed\")\n", - " \n", - " kx, ky = int(kx), int(ky)\n", - " img_blurred = cv.blur(img, ksize=(kx, ky))\n", - " \n", - " success, encoded = cv.imencode(\".jpeg\", img_blurred)\n", - " if not success:\n", - " raise ValueError(\"cv.imencode failed\")\n", - " \n", - " # Handle two output modes\n", - " if dst_rt: # GCS/Series output mode\n", - " dst_obj = json.loads(dst_rt)\n", - " dst_url = dst_obj[\"access_urls\"][\"write_url\"]\n", - " \n", - " requests.put(dst_url, data=encoded.tobytes(), headers={\"Content-Type\": \"image/jpeg\"}, timeout=30).raise_for_status()\n", - " \n", - " uri = dst_obj[\"objectref\"][\"uri\"]\n", - " return uri\n", - " \n", - " else: # BigQuery bytes output mode \n", - " image_bytes = encoded.tobytes()\n", - " return base64.b64encode(image_bytes).decode()\n", - "\n", - "def apply_transformation(series, dst_folder, udf, *args, verbose=False):\n", - " import os\n", - " dst_folder = os.path.join(dst_folder, \"\")\n", - " # Fetch metadata to get the URI\n", - " metadata = bbq.obj.fetch_metadata(series)\n", - " current_uri = metadata.struct.field(\"uri\")\n", - " dst_uri = current_uri.str.replace(r\"^.*\\/(.*)$\", rf\"{dst_folder}\\1\", regex=True)\n", - " dst_blob = dst_uri.str.to_blob(connection=FULL_CONNECTION_ID)\n", - " df_transform = bpd.DataFrame({\n", - " \"src_rt\": get_runtime_json_str(series, mode=\"R\"),\n", - " \"dst_rt\": get_runtime_json_str(dst_blob, mode=\"RW\"),\n", - " })\n", - " res = df_transform[[\"src_rt\", \"dst_rt\"]].apply(\n", - " udf, axis=1, args=args\n", - " )\n", - " return res if verbose else res.str.to_blob(connection=FULL_CONNECTION_ID)\n", - "\n", - "# Apply transformations\n", - "df_image[\"blurred\"] = apply_transformation(\n", - " df_image[\"image\"], f\"gs://{OUTPUT_BUCKET}/image_blur_transformed/\",\n", - " image_blur, 20, 20\n", - ")\n", - "df_image[[\"image\", \"blurred\"]]" + "text/plain": [ + " image \\\n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "\n", + " blurred \n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:3... \n", + "\n", + "[5 rows x 2 columns]" ] - }, + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Construct the canonical connection ID\n", + "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", + "\n", + "@bpd.udf(\n", + " input_types=[str, str, int, int],\n", + " output_type=str,\n", + " dataset=DATASET_ID,\n", + " name=\"image_blur\",\n", + " bigquery_connection=FULL_CONNECTION_ID,\n", + " packages=[\"opencv-python\", \"numpy\", \"requests\"],\n", + ")\n", + "def image_blur(src_rt: str, dst_rt: str, kx: int, ky: int) -> str:\n", + " import json\n", + " import cv2 as cv\n", + " import numpy as np\n", + " import requests\n", + " import base64\n", + "\n", + " src_obj = json.loads(src_rt)\n", + " src_url = src_obj[\"access_urls\"][\"read_url\"]\n", + " \n", + " response = requests.get(src_url, timeout=30)\n", + " response.raise_for_status()\n", + " \n", + " img = cv.imdecode(np.frombuffer(response.content, np.uint8), cv.IMREAD_UNCHANGED)\n", + " if img is None:\n", + " raise ValueError(\"cv.imdecode failed\")\n", + " \n", + " kx, ky = int(kx), int(ky)\n", + " img_blurred = cv.blur(img, ksize=(kx, ky))\n", + " \n", + " success, encoded = cv.imencode(\".jpeg\", img_blurred)\n", + " if not success:\n", + " raise ValueError(\"cv.imencode failed\")\n", + " \n", + " # Handle two output modes\n", + " if dst_rt: # GCS/Series output mode\n", + " dst_obj = json.loads(dst_rt)\n", + " dst_url = dst_obj[\"access_urls\"][\"write_url\"]\n", + " \n", + " requests.put(dst_url, data=encoded.tobytes(), headers={\"Content-Type\": \"image/jpeg\"}, timeout=30).raise_for_status()\n", + " \n", + " uri = dst_obj[\"objectref\"][\"uri\"]\n", + " return uri\n", + " \n", + " else: # BigQuery bytes output mode \n", + " image_bytes = encoded.tobytes()\n", + " return base64.b64encode(image_bytes).decode()\n", + "\n", + "def apply_transformation(series, dst_folder, udf, *args, verbose=False):\n", + " import os\n", + " dst_folder = os.path.join(dst_folder, \"\")\n", + " # Fetch metadata to get the URI\n", + " metadata = bbq.obj.fetch_metadata(series)\n", + " current_uri = metadata.struct.field(\"uri\")\n", + " dst_uri = current_uri.str.replace(r\"^.*\\/(.*)$\", rf\"{dst_folder}\\1\", regex=True)\n", + " dst_blob = dst_uri.str.to_blob(connection=FULL_CONNECTION_ID)\n", + " df_transform = bpd.DataFrame({\n", + " \"src_rt\": get_runtime_json_str(series, mode=\"R\"),\n", + " \"dst_rt\": get_runtime_json_str(dst_blob, mode=\"RW\"),\n", + " })\n", + " res = df_transform[[\"src_rt\", \"dst_rt\"]].apply(\n", + " udf, axis=1, args=args\n", + " )\n", + " return res if verbose else res.str.to_blob(connection=FULL_CONNECTION_ID)\n", + "\n", + "# Apply transformations\n", + "df_image[\"blurred\"] = apply_transformation(\n", + " df_image[\"image\"], f\"gs://{OUTPUT_BUCKET}/image_blur_transformed/\",\n", + " image_blur, 20, 20\n", + ")\n", + "df_image[[\"image\", \"blurred\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Euk5saeVVdTP" + }, + "source": [ + "### 4. Use LLM models to ask questions and generate embeddings on images" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "mRUGfcaFVW-3" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "Euk5saeVVdTP" - }, - "source": [ - "### 4. Use LLM models to ask questions and generate embeddings on images" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:183: FutureWarning: Since upgrading the default model can cause unintended breakages, the\n", + "default model will be removed in BigFrames 3.0. Please supply an\n", + "explicit model to avoid this message.\n", + " return method(*args, **kwargs)\n" + ] + } + ], + "source": [ + "from bigframes.ml import llm\n", + "gemini = llm.GeminiTextGenerator()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 657 }, + "id": "DNFP7CbjWdR9", + "outputId": "3f90a062-0abc-4bce-f53c-db57b06a14b9" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "mRUGfcaFVW-3" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:183: FutureWarning: Since upgrading the default model can cause unintended breakages, the\n", - "default model will be removed in BigFrames 3.0. Please supply an\n", - "explicit model to avoid this message.\n", - " return method(*args, **kwargs)\n" - ] - } - ], - "source": [ - "from bigframes.ml import llm\n", - "gemini = llm.GeminiTextGenerator()" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 657 - }, - "id": "DNFP7CbjWdR9", - "outputId": "3f90a062-0abc-4bce-f53c-db57b06a14b9" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ml_generate_text_llm_resultimage
0The item is a container of K9 Guard Dog Paw Balm.
1The item is K9 Guard Dog Hot Spot Spray.
2The image contains three bags of food, likely for small animals like rabbits or guinea pigs. They are labeled \"Timoth Hay Lend Variety Plend\", \"Herbal Greeıs Mix Variety Blend\", and \"Berry & Blossom Treat Blend\", all under the brand \"Fluffy Buns.\" The bags are yellow, green, and purple, respectively. Each bag has a pile of its contents beneath it.
3The item is a cat tree.\\n
4The item is a bag of bird seed. Specifically, it's labeled \"Chirpy Seed\", \"Deluxe Bird Food\".\\n
\n", - "

5 rows × 2 columns

\n", - "
[5 rows x 2 columns in total]" - ], - "text/plain": [ - " ml_generate_text_llm_result \\\n", - "0 The item is a container of K9 Guard Dog Paw Balm. \n", - "1 The item is K9 Guard Dog Hot Spot Spray. \n", - "2 The image contains three bags of food, likely ... \n", - "3 The item is a cat tree.\\n \n", - "4 The item is a bag of bird seed. Specifically, ... \n", - "\n", - " image \n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "\n", - "[5 rows x 2 columns]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ml_generate_text_llm_resultimage
0The item is a container of K9 Guard Dog Paw Balm.
1The item is K9 Guard Dog Hot Spot Spray.
2The image contains three bags of food, likely for small animals like rabbits or guinea pigs. They are labeled \"Timoth Hay Lend Variety Plend\", \"Herbal Greeıs Mix Variety Blend\", and \"Berry & Blossom Treat Blend\", all under the brand \"Fluffy Buns.\" The bags are yellow, green, and purple, respectively. Each bag has a pile of its contents beneath it.
3The item is a cat tree.\\n
4The item is a bag of bird seed. Specifically, it's labeled \"Chirpy Seed\", \"Deluxe Bird Food\".\\n
\n", + "

5 rows × 2 columns

\n", + "
[5 rows x 2 columns in total]" ], - "source": [ - "# Ask the same question on the images\n", - "answer = gemini.predict(df_image, prompt=[\"what item is it?\", df_image[\"image\"]])\n", - "answer[[\"ml_generate_text_llm_result\", \"image\"]]" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "IG3J3HsKhyBY" - }, - "outputs": [], - "source": [ - "# Ask different questions\n", - "df_image[\"question\"] = [\n", - " \"what item is it?\",\n", - " \"what color is the picture?\",\n", - " \"what is the product name?\",\n", - " \"is it for pets?\",\n", - " \"what is the weight of the product?\",\n", - "]" + "text/plain": [ + " ml_generate_text_llm_result \\\n", + "0 The item is a container of K9 Guard Dog Paw Balm. \n", + "1 The item is K9 Guard Dog Hot Spot Spray. \n", + "2 The image contains three bags of food, likely ... \n", + "3 The item is a cat tree.\\n \n", + "4 The item is a bag of bird seed. Specifically, ... \n", + "\n", + " image \n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "\n", + "[5 rows x 2 columns]" ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ask the same question on the images\n", + "answer = gemini.predict(df_image, prompt=[\"what item is it?\", df_image[\"image\"]])\n", + "answer[[\"ml_generate_text_llm_result\", \"image\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "IG3J3HsKhyBY" + }, + "outputs": [], + "source": [ + "# Ask different questions\n", + "df_image[\"question\"] = [\n", + " \"what item is it?\",\n", + " \"what color is the picture?\",\n", + " \"what is the product name?\",\n", + " \"is it for pets?\",\n", + " \"what is the weight of the product?\",\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 657 }, + "id": "qKOb765IiVuD", + "outputId": "731bafad-ea29-463f-c8c1-cb7acfd70e5d" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 657 - }, - "id": "qKOb765IiVuD", - "outputId": "731bafad-ea29-463f-c8c1-cb7acfd70e5d" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ml_generate_text_llm_resultimage
0The item is a container of Dog Paw Balm.
1The picture contains many colors, including white, black, green, and a bright blue. The product label predominantly features a bright blue hue. The background is a solid gray.
2Here are the product names from the image:\\n\\n* **Timoth Hay Lend Variety Plend** is the product in the yellow bag.\\n* **Herbal Greeıs Mix Variety Blend** is the product in the green bag.\\n* **Berry & Blossom Treat Blend** is the product in the purple bag.
3Yes, it is for pets. It appears to be a cat tree or scratching post.\\n
4The image shows that the weight of the product is 15 oz/ 257g.
\n", - "

5 rows × 2 columns

\n", - "
[5 rows x 2 columns in total]" - ], - "text/plain": [ - " ml_generate_text_llm_result \\\n", - "0 The item is a container of Dog Paw Balm. \n", - "1 The picture contains many colors, including wh... \n", - "2 Here are the product names from the image:\\n\\n... \n", - "3 Yes, it is for pets. It appears to be a cat tr... \n", - "4 The image shows that the weight of the product... \n", - "\n", - " image \n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "\n", - "[5 rows x 2 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "answer_alt = gemini.predict(df_image, prompt=[df_image[\"question\"], df_image[\"image\"]])\n", - "answer_alt[[\"ml_generate_text_llm_result\", \"image\"]]" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n" + ] }, { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 300 - }, - "id": "KATVv2CO5RT1", - "outputId": "6ec01f27-70b6-4f69-c545-e5e3c879480c" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:183: FutureWarning: Since upgrading the default model can cause unintended breakages, the\n", - "default model will be removed in BigFrames 3.0. Please supply an\n", - "explicit model to avoid this message.\n", - " return method(*args, **kwargs)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", - " return prop(*args, **kwargs)\n", - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\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", - " \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", - "
ml_generate_embedding_resultml_generate_embedding_statusml_generate_embedding_start_secml_generate_embedding_end_seccontent
0[ 0.00638822 0.01666385 0.00451817 ... -0.02...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
1[ 0.00973976 0.02148137 0.0024429 ... 0.00...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
2[ 0.01195884 0.02139394 0.05968047 ... -0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
3[-0.02621161 0.02797648 0.04416926 ... -0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
4[ 0.05918628 0.0125137 0.01907336 ... 0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
\n", - "

5 rows × 5 columns

\n", - "
[5 rows x 5 columns in total]" - ], - "text/plain": [ - " ml_generate_embedding_result \\\n", - "0 [ 0.00638822 0.01666385 0.00451817 ... -0.02... \n", - "1 [ 0.00973976 0.02148137 0.0024429 ... 0.00... \n", - "2 [ 0.01195884 0.02139394 0.05968047 ... -0.01... \n", - "3 [-0.02621161 0.02797648 0.04416926 ... -0.01... \n", - "4 [ 0.05918628 0.0125137 0.01907336 ... 0.01... \n", - "\n", - " ml_generate_embedding_status ml_generate_embedding_start_sec \\\n", - "0 \n", - "1 \n", - "2 \n", - "3 \n", - "4 \n", - "\n", - " ml_generate_embedding_end_sec \\\n", - "0 \n", - "1 \n", - "2 \n", - "3 \n", - "4 \n", - "\n", - " content \n", - "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", - "\n", - "[5 rows x 5 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ml_generate_text_llm_resultimage
0The item is a container of Dog Paw Balm.
1The picture contains many colors, including white, black, green, and a bright blue. The product label predominantly features a bright blue hue. The background is a solid gray.
2Here are the product names from the image:\\n\\n* **Timoth Hay Lend Variety Plend** is the product in the yellow bag.\\n* **Herbal Greeıs Mix Variety Blend** is the product in the green bag.\\n* **Berry & Blossom Treat Blend** is the product in the purple bag.
3Yes, it is for pets. It appears to be a cat tree or scratching post.\\n
4The image shows that the weight of the product is 15 oz/ 257g.
\n", + "

5 rows × 2 columns

\n", + "
[5 rows x 2 columns in total]" ], - "source": [ - "# Generate embeddings.\n", - "embed_model = llm.MultimodalEmbeddingGenerator()\n", - "embeddings = embed_model.predict(df_image[\"image\"])\n", - "embeddings" + "text/plain": [ + " ml_generate_text_llm_result \\\n", + "0 The item is a container of Dog Paw Balm. \n", + "1 The picture contains many colors, including wh... \n", + "2 Here are the product names from the image:\\n\\n... \n", + "3 Yes, it is for pets. It appears to be a cat tr... \n", + "4 The image shows that the weight of the product... \n", + "\n", + " image \n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "\n", + "[5 rows x 2 columns]" ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer_alt = gemini.predict(df_image, prompt=[df_image[\"question\"], df_image[\"image\"]])\n", + "answer_alt[[\"ml_generate_text_llm_result\", \"image\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 300 }, + "id": "KATVv2CO5RT1", + "outputId": "6ec01f27-70b6-4f69-c545-e5e3c879480c" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "iRUi8AjG7cIf" - }, - "source": [ - "### 5. PDF extraction and chunking function\n", - "\n", - "This section demonstrates how to extract text and chunk text from PDF files using custom BigQuery Python UDFs and the `pypdf` library." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:183: FutureWarning: Since upgrading the default model can cause unintended breakages, the\n", + "default model will be removed in BigFrames 3.0. Please supply an\n", + "explicit model to avoid this message.\n", + " return method(*args, **kwargs)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/logging/log_adapter.py:229: ApiDeprecationWarning: The blob accessor is deprecated and will be removed in a future release. Use bigframes.bigquery.obj functions instead.\n", + " return prop(*args, **kwargs)\n", + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n" + ] }, { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", - " return global_session.with_default_session(\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", + " \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", + "
ml_generate_embedding_resultml_generate_embedding_statusml_generate_embedding_start_secml_generate_embedding_end_seccontent
0[ 0.00638822 0.01666385 0.00451817 ... -0.02...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
1[ 0.00973976 0.02148137 0.0024429 ... 0.00...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
2[ 0.01195884 0.02139394 0.05968047 ... -0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
3[-0.02621161 0.02797648 0.04416926 ... -0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
4[ 0.05918628 0.0125137 0.01907336 ... 0.01...<NA><NA>{\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4...
\n", + "

5 rows × 5 columns

\n", + "
[5 rows x 5 columns in total]" ], - "source": [ - "# Construct the canonical connection ID\n", - "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", - "\n", - "@bpd.udf(\n", - " input_types=[str],\n", - " output_type=str,\n", - " dataset=DATASET_ID,\n", - " name=\"pdf_extract\",\n", - " bigquery_connection=FULL_CONNECTION_ID,\n", - " packages=[\"pypdf\", \"requests\", \"cryptography\"],\n", - ")\n", - "def pdf_extract(src_obj_ref_rt: str) -> str:\n", - " import io\n", - " import json\n", - " from pypdf import PdfReader\n", - " import requests\n", - " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", - " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", - " response = requests.get(src_url, timeout=30, stream=True)\n", - " response.raise_for_status()\n", - " pdf_bytes = response.content\n", - " pdf_file = io.BytesIO(pdf_bytes)\n", - " reader = PdfReader(pdf_file, strict=False)\n", - " all_text = \"\"\n", - " for page in reader.pages:\n", - " page_extract_text = page.extract_text()\n", - " if page_extract_text:\n", - " all_text += page_extract_text\n", - " return all_text\n", - "\n", - "@bpd.udf(\n", - " input_types=[str, int, int],\n", - " output_type=list[str],\n", - " dataset=DATASET_ID,\n", - " name=\"pdf_chunk\",\n", - " bigquery_connection=FULL_CONNECTION_ID,\n", - " packages=[\"pypdf\", \"requests\", \"cryptography\"],\n", - ")\n", - "def pdf_chunk(src_obj_ref_rt: str, chunk_size: int, overlap_size: int) -> list[str]:\n", - " import io\n", - " import json\n", - " from pypdf import PdfReader\n", - " import requests\n", - " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", - " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", - " response = requests.get(src_url, timeout=30, stream=True)\n", - " response.raise_for_status()\n", - " pdf_bytes = response.content\n", - " pdf_file = io.BytesIO(pdf_bytes)\n", - " reader = PdfReader(pdf_file, strict=False)\n", - " all_text_chunks = []\n", - " curr_chunk = \"\"\n", - " for page in reader.pages:\n", - " page_text = page.extract_text()\n", - " if page_text:\n", - " curr_chunk += page_text\n", - " while len(curr_chunk) >= chunk_size:\n", - " split_idx = curr_chunk.rfind(\" \", 0, chunk_size)\n", - " if split_idx == -1:\n", - " split_idx = chunk_size\n", - " actual_chunk = curr_chunk[:split_idx]\n", - " all_text_chunks.append(actual_chunk)\n", - " overlap = curr_chunk[split_idx + 1 : split_idx + 1 + overlap_size]\n", - " curr_chunk = overlap + curr_chunk[split_idx + 1 + overlap_size :]\n", - " if curr_chunk:\n", - " all_text_chunks.append(curr_chunk)\n", - " return all_text_chunks" + "text/plain": [ + " ml_generate_embedding_result \\\n", + "0 [ 0.00638822 0.01666385 0.00451817 ... -0.02... \n", + "1 [ 0.00973976 0.02148137 0.0024429 ... 0.00... \n", + "2 [ 0.01195884 0.02139394 0.05968047 ... -0.01... \n", + "3 [-0.02621161 0.02797648 0.04416926 ... -0.01... \n", + "4 [ 0.05918628 0.0125137 0.01907336 ... 0.01... \n", + "\n", + " ml_generate_embedding_status ml_generate_embedding_start_sec \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "\n", + " ml_generate_embedding_end_sec \\\n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + "\n", + " content \n", + "0 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "1 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "2 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "3 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "4 {\"access_urls\":{\"expiry_time\":\"2026-02-21T01:4... \n", + "\n", + "[5 rows x 5 columns]" ] - }, + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Generate embeddings.\n", + "embed_model = llm.MultimodalEmbeddingGenerator()\n", + "embeddings = embed_model.predict(df_image[\"image\"])\n", + "embeddings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iRUi8AjG7cIf" + }, + "source": [ + "### 5. PDF extraction and chunking function\n", + "\n", + "This section demonstrates how to extract text and chunk text from PDF files using custom BigQuery Python UDFs and the `pypdf` library." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
extracted_textchunked
0CritterCuisine Pro 5000 - Automatic Pet Feeder...[\"CritterCuisine Pro 5000 - Automatic Pet Feed...
\n", - "

1 rows × 2 columns

\n", - "
[1 rows x 2 columns in total]" - ], - "text/plain": [ - " extracted_text \\\n", - "0 CritterCuisine Pro 5000 - Automatic Pet Feeder... \n", - "\n", - " chunked \n", - "0 [\"CritterCuisine Pro 5000 - Automatic Pet Feed... \n", - "\n", - "[1 rows x 2 columns]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df_pdf = bpd.from_glob_path(\"gs://cloud-samples-data/bigquery/tutorials/cymbal-pets/documents/*\", name=\"pdf\")\n", - "\n", - "# Generate a JSON string containing the runtime information (including signed read URLs)\n", - "access_urls = get_runtime_json_str(df_pdf[\"pdf\"], mode=\"R\")\n", - "\n", - "# Apply PDF extraction\n", - "df_pdf[\"extracted_text\"] = access_urls.apply(pdf_extract)\n", - "\n", - "# Apply PDF chunking\n", - "df_pdf[\"chunked\"] = access_urls.apply(pdf_chunk, args=(2000, 200))\n", - "\n", - "df_pdf[[\"extracted_text\", \"chunked\"]]" - ] - }, + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", + " return global_session.with_default_session(\n" + ] + } + ], + "source": [ + "# Construct the canonical connection ID\n", + "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", + "\n", + "@bpd.udf(\n", + " input_types=[str],\n", + " output_type=str,\n", + " dataset=DATASET_ID,\n", + " name=\"pdf_extract\",\n", + " bigquery_connection=FULL_CONNECTION_ID,\n", + " packages=[\"pypdf\", \"requests\", \"cryptography\"],\n", + ")\n", + "def pdf_extract(src_obj_ref_rt: str) -> str:\n", + " import io\n", + " import json\n", + " from pypdf import PdfReader\n", + " import requests\n", + " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", + " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", + " response = requests.get(src_url, timeout=30, stream=True)\n", + " response.raise_for_status()\n", + " pdf_bytes = response.content\n", + " pdf_file = io.BytesIO(pdf_bytes)\n", + " reader = PdfReader(pdf_file, strict=False)\n", + " all_text = \"\"\n", + " for page in reader.pages:\n", + " page_extract_text = page.extract_text()\n", + " if page_extract_text:\n", + " all_text += page_extract_text\n", + " return all_text\n", + "\n", + "@bpd.udf(\n", + " input_types=[str, int, int],\n", + " output_type=list[str],\n", + " dataset=DATASET_ID,\n", + " name=\"pdf_chunk\",\n", + " bigquery_connection=FULL_CONNECTION_ID,\n", + " packages=[\"pypdf\", \"requests\", \"cryptography\"],\n", + ")\n", + "def pdf_chunk(src_obj_ref_rt: str, chunk_size: int, overlap_size: int) -> list[str]:\n", + " import io\n", + " import json\n", + " from pypdf import PdfReader\n", + " import requests\n", + " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", + " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", + " response = requests.get(src_url, timeout=30, stream=True)\n", + " response.raise_for_status()\n", + " pdf_bytes = response.content\n", + " pdf_file = io.BytesIO(pdf_bytes)\n", + " reader = PdfReader(pdf_file, strict=False)\n", + " all_text_chunks = []\n", + " curr_chunk = \"\"\n", + " for page in reader.pages:\n", + " page_text = page.extract_text()\n", + " if page_text:\n", + " curr_chunk += page_text\n", + " while len(curr_chunk) >= chunk_size:\n", + " split_idx = curr_chunk.rfind(\" \", 0, chunk_size)\n", + " if split_idx == -1:\n", + " split_idx = chunk_size\n", + " actual_chunk = curr_chunk[:split_idx]\n", + " all_text_chunks.append(actual_chunk)\n", + " overlap = curr_chunk[split_idx + 1 : split_idx + 1 + overlap_size]\n", + " curr_chunk = overlap + curr_chunk[split_idx + 1 + overlap_size :]\n", + " if curr_chunk:\n", + " all_text_chunks.append(curr_chunk)\n", + " return all_text_chunks" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
0    CritterCuisine Pro 5000 - Automatic Pet Feeder...\n",
-              "0    on a level, stable surface to prevent tipping....\n",
-              "0    included)\\nto maintain the schedule during pow...\n",
-              "0    digits for Meal 1 will flash.\\n\u0000. Use the UP/D...\n",
-              "0    paperclip) for 5\\nseconds. This will reset all...\n",
-              "0    unit with a damp cloth. Do not immerse the bas...\n",
-              "0    continues,\\ncontact customer support.\\nE2: Foo...
" - ], - "text/plain": [ - "0 CritterCuisine Pro 5000 - Automatic Pet Feeder...\n", - "0 on a level, stable surface to prevent tipping....\n", - "0 included)\\nto maintain the schedule during pow...\n", - "0 digits for Meal 1 will flash.\\n\u0000. Use the UP/D...\n", - "0 paperclip) for 5\\nseconds. This will reset all...\n", - "0 unit with a damp cloth. Do not immerse the bas...\n", - "0 continues,\\ncontact customer support.\\nE2: Foo...\n", - "Name: chunked, dtype: string" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
extracted_textchunked
0CritterCuisine Pro 5000 - Automatic Pet Feeder...[\"CritterCuisine Pro 5000 - Automatic Pet Feed...
\n", + "

1 rows × 2 columns

\n", + "
[1 rows x 2 columns in total]" ], - "source": [ - "# Explode the chunks to see each chunk as a separate row\n", - "chunked = df_pdf[\"chunked\"].explode()\n", - "chunked" + "text/plain": [ + " extracted_text \\\n", + "0 CritterCuisine Pro 5000 - Automatic Pet Feeder... \n", + "\n", + " chunked \n", + "0 [\"CritterCuisine Pro 5000 - Automatic Pet Feed... \n", + "\n", + "[1 rows x 2 columns]" ] - }, + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_pdf = bpd.from_glob_path(\"gs://cloud-samples-data/bigquery/tutorials/cymbal-pets/documents/*\", name=\"pdf\")\n", + "\n", + "# Generate a JSON string containing the runtime information (including signed read URLs)\n", + "access_urls = get_runtime_json_str(df_pdf[\"pdf\"], mode=\"R\")\n", + "\n", + "# Apply PDF extraction\n", + "df_pdf[\"extracted_text\"] = access_urls.apply(pdf_extract)\n", + "\n", + "# Apply PDF chunking\n", + "df_pdf[\"chunked\"] = access_urls.apply(pdf_chunk, args=(2000, 200))\n", + "\n", + "df_pdf[[\"extracted_text\", \"chunked\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 6. Audio transcribe" + "data": { + "text/html": [ + "
0    CritterCuisine Pro 5000 - Automatic Pet Feeder...\n",
+       "0    on a level, stable surface to prevent tipping....\n",
+       "0    included)\\nto maintain the schedule during pow...\n",
+       "0    digits for Meal 1 will flash.\\n\u0000. Use the UP/D...\n",
+       "0    paperclip) for 5\\nseconds. This will reset all...\n",
+       "0    unit with a damp cloth. Do not immerse the bas...\n",
+       "0    continues,\\ncontact customer support.\\nE2: Foo...
" + ], + "text/plain": [ + "0 CritterCuisine Pro 5000 - Automatic Pet Feeder...\n", + "0 on a level, stable surface to prevent tipping....\n", + "0 included)\\nto maintain the schedule during pow...\n", + "0 digits for Meal 1 will flash.\\n\u0000. Use the UP/D...\n", + "0 paperclip) for 5\\nseconds. This will reset all...\n", + "0 unit with a damp cloth. Do not immerse the bas...\n", + "0 continues,\\ncontact customer support.\\nE2: Foo...\n", + "Name: chunked, dtype: string" ] - }, + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Explode the chunks to see each chunk as a separate row\n", + "chunked = df_pdf[\"chunked\"].explode()\n", + "chunked" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6. Audio transcribe" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "audio_gcs_path = \"gs://bigframes_blob_test/audio/*\"\n", + "df = bpd.from_glob_path(audio_gcs_path, name=\"audio\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "audio_gcs_path = \"gs://bigframes_blob_test/audio/*\"\n", - "df = bpd.from_glob_path(audio_gcs_path, name=\"audio\")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", + "instead of using `db_dtypes` in the future when available in pandas\n", + "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", + " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n" + ] }, { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/dtypes.py:990: JSONDtypeWarning: JSON columns will be represented as pandas.ArrowDtype(pyarrow.json_())\n", - "instead of using `db_dtypes` in the future when available in pandas\n", - "(https://github.com/pandas-dev/pandas/issues/60958) and pyarrow.\n", - " warnings.warn(msg, bigframes.exceptions.JSONDtypeWarning)\n" - ] - }, - { - "data": { - "text/html": [ - "
0    Now, as all books, not primarily intended as p...
" - ], - "text/plain": [ - "0 Now, as all books, not primarily intended as p...\n", - "Name: transcribed_content, dtype: string" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
0    Now, as all books, not primarily intended as p...
" ], - "source": [ - "# The audio_transcribe function is a convenience wrapper around bigframes.bigquery.ai.generate.\n", - "# Here's how to perform the same operation directly:\n", - "\n", - "audio_series = df[\"audio\"]\n", - "prompt_text = (\n", - " \"**Task:** Transcribe the provided audio. **Instructions:** - Your response \"\n", - " \"must contain only the verbatim transcription of the audio. - Do not include \"\n", - " \"any introductory text, summaries, or conversational filler in your response. \"\n", - " \"The output should begin directly with the first word of the audio.\"\n", - ")\n", - "\n", - "# Convert the audio series to the runtime representation required by the model.\n", - "# This involves fetching metadata and getting a signed access URL.\n", - "audio_metadata = bbq.obj.fetch_metadata(audio_series)\n", - "audio_runtime = bbq.obj.get_access_url(audio_metadata, mode=\"R\")\n", - "\n", - "transcribed_results = bbq.ai.generate(\n", - " prompt=(prompt_text, audio_runtime),\n", - " endpoint=\"gemini-2.0-flash-001\",\n", - " model_params={\"generationConfig\": {\"temperature\": 0.0}},\n", - ")\n", - "\n", - "transcribed_series = transcribed_results.struct.field(\"result\").rename(\"transcribed_content\")\n", - "transcribed_series" + "text/plain": [ + "0 Now, as all books, not primarily intended as p...\n", + "Name: transcribed_content, dtype: string" ] - }, + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The audio_transcribe function is a convenience wrapper around bigframes.bigquery.ai.generate.\n", + "# Here's how to perform the same operation directly:\n", + "\n", + "audio_series = df[\"audio\"]\n", + "prompt_text = (\n", + " \"**Task:** Transcribe the provided audio. **Instructions:** - Your response \"\n", + " \"must contain only the verbatim transcription of the audio. - Do not include \"\n", + " \"any introductory text, summaries, or conversational filler in your response. \"\n", + " \"The output should begin directly with the first word of the audio.\"\n", + ")\n", + "\n", + "# Convert the audio series to the runtime representation required by the model.\n", + "# This involves fetching metadata and getting a signed access URL.\n", + "audio_metadata = bbq.obj.fetch_metadata(audio_series)\n", + "audio_runtime = bbq.obj.get_access_url(audio_metadata, mode=\"R\")\n", + "\n", + "transcribed_results = bbq.ai.generate(\n", + " prompt=(prompt_text, audio_runtime),\n", + " endpoint=\"gemini-2.0-flash-001\",\n", + " model_params={\"generationConfig\": {\"temperature\": 0.0}},\n", + ")\n", + "\n", + "transcribed_series = transcribed_results.struct.field(\"result\").rename(\"transcribed_content\")\n", + "transcribed_series" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
0    {'status': '', 'content': 'Now, as all books, ...
" - ], - "text/plain": [ - "0 {'status': '', 'content': 'Now, as all books, ...\n", - "Name: transcription_results, dtype: struct[pyarrow]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
0    {'status': '', 'content': 'Now, as all books, ...
" ], - "source": [ - "# To get verbose results (including status), we can extract both fields from the result struct.\n", - "transcribed_content_series = transcribed_results.struct.field(\"result\")\n", - "transcribed_status_series = transcribed_results.struct.field(\"status\")\n", - "\n", - "transcribed_series_verbose = bpd.DataFrame(\n", - " {\n", - " \"status\": transcribed_status_series,\n", - " \"content\": transcribed_content_series,\n", - " }\n", - ")\n", - "# Package as a struct for consistent display\n", - "transcribed_series_verbose = bbq.struct(transcribed_series_verbose).rename(\"transcription_results\")\n", - "transcribed_series_verbose" + "text/plain": [ + "0 {'status': '', 'content': 'Now, as all books, ...\n", + "Name: transcription_results, dtype: struct[pyarrow]" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 7. Extract EXIF metadata from images" - ] - }, + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# To get verbose results (including status), we can extract both fields from the result struct.\n", + "transcribed_content_series = transcribed_results.struct.field(\"result\")\n", + "transcribed_status_series = transcribed_results.struct.field(\"status\")\n", + "\n", + "transcribed_series_verbose = bpd.DataFrame(\n", + " {\n", + " \"status\": transcribed_status_series,\n", + " \"content\": transcribed_content_series,\n", + " }\n", + ")\n", + "# Package as a struct for consistent display\n", + "transcribed_series_verbose = bbq.struct(transcribed_series_verbose).rename(\"transcription_results\")\n", + "transcribed_series_verbose" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7. Extract EXIF metadata from images" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section demonstrates how to extract EXIF metadata from images using a custom BigQuery Python UDF and the `Pillow` library." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This section demonstrates how to extract EXIF metadata from images using a custom BigQuery Python UDF and the `Pillow` library." - ] - }, + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", + " return global_session.with_default_session(\n" + ] + } + ], + "source": [ + "# Construct the canonical connection ID\n", + "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", + "\n", + "@bpd.udf(\n", + " input_types=[str],\n", + " output_type=str,\n", + " dataset=DATASET_ID,\n", + " name=\"extract_exif\",\n", + " bigquery_connection=FULL_CONNECTION_ID,\n", + " packages=[\"pillow\", \"requests\"],\n", + " max_batching_rows=8192,\n", + " container_cpu=0.33,\n", + " container_memory=\"512Mi\"\n", + ")\n", + "def extract_exif(src_obj_ref_rt: str) -> str:\n", + " import io\n", + " import json\n", + " from PIL import ExifTags, Image\n", + " import requests\n", + " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", + " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", + " response = requests.get(src_url, timeout=30)\n", + " bts = response.content\n", + " image = Image.open(io.BytesIO(bts))\n", + " exif_data = image.getexif()\n", + " exif_dict = {}\n", + " if exif_data:\n", + " for tag, value in exif_data.items():\n", + " tag_name = ExifTags.TAGS.get(tag, tag)\n", + " exif_dict[tag_name] = value\n", + " return json.dumps(exif_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/pandas/__init__.py:151: PreviewWarning: udf is in preview.\n", - " return global_session.with_default_session(\n" - ] - } - ], - "source": [ - "# Construct the canonical connection ID\n", - "FULL_CONNECTION_ID = f\"{PROJECT}.{LOCATION}.bigframes-default-connection\"\n", - "\n", - "@bpd.udf(\n", - " input_types=[str],\n", - " output_type=str,\n", - " dataset=DATASET_ID,\n", - " name=\"extract_exif\",\n", - " bigquery_connection=FULL_CONNECTION_ID,\n", - " packages=[\"pillow\", \"requests\"],\n", - " max_batching_rows=8192,\n", - " container_cpu=0.33,\n", - " container_memory=\"512Mi\"\n", - ")\n", - "def extract_exif(src_obj_ref_rt: str) -> str:\n", - " import io\n", - " import json\n", - " from PIL import ExifTags, Image\n", - " import requests\n", - " src_obj_ref_rt_json = json.loads(src_obj_ref_rt)\n", - " src_url = src_obj_ref_rt_json[\"access_urls\"][\"read_url\"]\n", - " response = requests.get(src_url, timeout=30)\n", - " bts = response.content\n", - " image = Image.open(io.BytesIO(bts))\n", - " exif_data = image.getexif()\n", - " exif_dict = {}\n", - " if exif_data:\n", - " for tag, value in exif_data.items():\n", - " tag_name = ExifTags.TAGS.get(tag, tag)\n", - " exif_dict[tag_name] = value\n", - " return json.dumps(exif_dict)" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/utils.py:228: PreviewWarning: The JSON-related API `parse_json` is in preview. Its behavior may\n", + "change in future versions.\n", + " warnings.warn(bfe.format_message(msg), category=bfe.PreviewWarning)\n" + ] }, { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/google/home/shuowei/src/python-bigquery-dataframes/bigframes/core/utils.py:228: PreviewWarning: The JSON-related API `parse_json` is in preview. Its behavior may\n", - "change in future versions.\n", - " warnings.warn(bfe.format_message(msg), category=bfe.PreviewWarning)\n" - ] - }, - { - "data": { - "text/html": [ - "
0    {\"ExifOffset\":47,\"Make\":\"MyCamera\"}
" - ], - "text/plain": [ - "0 {\"ExifOffset\":47,\"Make\":\"MyCamera\"}\n", - "Name: blob_col, dtype: extension>[pyarrow]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
0    {\"ExifOffset\":47,\"Make\":\"MyCamera\"}
" ], - "source": [ - "# Create a Multimodal DataFrame from the sample image URIs\n", - "exif_image_df = bpd.from_glob_path(\n", - " \"gs://bigframes_blob_test/images_exif/*\",\n", - " name=\"blob_col\",\n", - ")\n", - "\n", - "# Generate a JSON string containing the runtime information (including signed read URLs)\n", - "# This allows the UDF to download the images from Google Cloud Storage\n", - "access_urls = get_runtime_json_str(exif_image_df[\"blob_col\"], mode=\"R\")\n", - "\n", - "# Apply the BigQuery Python UDF to the runtime JSON strings\n", - "# We cast to string to ensure the input matches the UDF's signature\n", - "exif_json = access_urls.astype(str).apply(extract_exif)\n", - "\n", - "# Parse the resulting JSON strings back into a structured JSON type for easier access\n", - "exif_data = bbq.parse_json(exif_json)\n", - "\n", - "exif_data" + "text/plain": [ + "0 {\"ExifOffset\":47,\"Make\":\"MyCamera\"}\n", + "Name: blob_col, dtype: extension>[pyarrow]" ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "display_name": "venv (3.13.0)", - "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.13.0" - } + ], + "source": [ + "# Create a Multimodal DataFrame from the sample image URIs\n", + "exif_image_df = bpd.from_glob_path(\n", + " \"gs://bigframes_blob_test/images_exif/*\",\n", + " name=\"blob_col\",\n", + ")\n", + "\n", + "# Generate a JSON string containing the runtime information (including signed read URLs)\n", + "# This allows the UDF to download the images from Google Cloud Storage\n", + "access_urls = get_runtime_json_str(exif_image_df[\"blob_col\"], mode=\"R\")\n", + "\n", + "# Apply the BigQuery Python UDF to the runtime JSON strings\n", + "# We cast to string to ensure the input matches the UDF's signature\n", + "exif_json = access_urls.astype(str).apply(extract_exif)\n", + "\n", + "# Parse the resulting JSON strings back into a structured JSON type for easier access\n", + "exif_data = bbq.parse_json(exif_json)\n", + "\n", + "exif_data" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "venv (3.13.0)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "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.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/remote_functions/remote_function.ipynb b/notebooks/remote_functions/remote_function.ipynb index 4c0524d4026..a70d05ae062 100644 --- a/notebooks/remote_functions/remote_function.ipynb +++ b/notebooks/remote_functions/remote_function.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# Remote Functions" + ] + }, { "cell_type": "code", "execution_count": 19, diff --git a/notebooks/streaming/streaming_dataframe.ipynb b/notebooks/streaming/streaming_dataframe.ipynb index b7da0cfd077..e3dafa98195 100644 --- a/notebooks/streaming/streaming_dataframe.ipynb +++ b/notebooks/streaming/streaming_dataframe.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### BigFrames StreamingDataFrame\n", + "# BigFrames StreamingDataFrame", "bigframes.streaming.StreamingDataFrame is a special DataFrame type that allows simple operations and can create streaming jobs to process real-time data and reverse ETL output to Bigtable and Pub/Sub using [BigQuery continuous queries](https://cloud.google.com/bigquery/docs/continuous-queries-introduction).\n", "\n", "In this notebook, we will:\n", @@ -97,7 +97,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Create, select, filter and preview\n", + "## Create, select, filter and preview", "Create the StreamingDataFrame from a BigQuery table, select certain columns, filter rows and preview the output" ] }, diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb index d69aecd8c30..b28df7b0d7d 100644 --- a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb +++ b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb @@ -1,648 +1,648 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "id": "9GIt_orUtNvA" - }, - "outputs": [], - "source": [ - "# Copyright 2023 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": { - "id": "h7AT6h2ItNvD" - }, - "source": [ - "## Use BigQuery DataFrames to visualize COVID-19 data\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \"Colab Run in Colab\n", - " \n", - " \n", - " \n", - " \"GitHub\n", - " View on GitHub\n", - " \n", - " \n", - " \n", - " \"BQ\n", - " Open in BQ Studio\n", - " \n", - "
" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "n-MFJQxLtNvE" - }, - "source": [ - "## Overview\n", - "\n", - "The goal of this notebook is to demonstrate creating line graphs from a ~20 million-row BigQuery dataset using BigQuery DataFrames. We will first create a plain line graph using matplotlip, then we will downsample and download our data to create a graph with a line of best fit using seaborn.\n", - "\n", - "If you're like me, during 2020 (and/or later years) you often found yourself looking at charts like [these](https://health.google.com/covid-19/open-data/explorer/statistics) visualizing COVID-19 cases over time. For our first graph, we're going to recreate one of those charts by filtering, summing, and then graphing COVID-19 data from the United States. BigQuery DataFrame's default integration with matplotlib will get us a satisfying result for this first graph.\n", - "\n", - "For our second graph, though, we want to use a scatterplot with a line of best fit, something that matplotlib will not do for us automatically. So, we'll demonstrate how to downsample our data and use seaborn to make our plot. Our second graph will be of symptom-related search trends against new cases of COVID-19, so we'll see if searches for things like \"cough\" and \"fever\" are more common in the places and times where more new cases of COVID-19 occur." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "ffqBzbNztNvF" - }, - "source": [ - "### Dataset\n", - "\n", - "This notebook uses the [BigQuery COVID-19 Open Data](https://pantheon.corp.google.com/marketplace/product/bigquery-public-datasets/covid19-open-data). In this dataset, each row represents a new observation of the COVID-19 situation in a particular time and place. We will use the \"new_confirmed\" column, which contains the number of new COVID-19 cases at each observation, along with the \"search_trends_cough\", \"search_trends_fever\", and \"search_trends_bruise\" columns, which are [Google Trends](https://trends.google.com/trends/) data for searches related to cough, fever, and bruises. In the first section of the notebook, we will also use the \"country_code\" and \"date\" columns to compile one data point per day for a particular country." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Nf__tMR-tNvF" - }, - "source": [ - "### Costs\n", - "\n", - "This tutorial uses billable components of Google Cloud:\n", - "\n", - "* BigQuery (compute)\n", - "\n", - "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", - "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", - "to generate a cost estimate based on your projected usage." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "7_rsbkCktNvG" - }, - "source": [ - "## Before you begin\n", - "\n", - "### Set up your Google Cloud project\n", - "\n", - "**The following steps are required, regardless of your notebook environment.**\n", - "\n", - "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", - "\n", - "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", - "\n", - "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", - "\n", - "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "XZKC6iMFxmMG" - }, - "source": [ - "#### Set your project ID\n", - "\n", - "**If you don't know your project ID**, try the following:\n", - "* Run `gcloud config list`.\n", - "* Run `gcloud projects list`.\n", - "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "4aooKMmnxrWF" - }, - "outputs": [], - "source": [ - "PROJECT_ID = \"\" # @param {type:\"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pv5A8Tm-yC1U" - }, - "source": [ - "#### Set the region\n", - "\n", - "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "bk03Rt_HyGx-" - }, - "outputs": [], - "source": [ - "REGION = \"US\" # @param {type: \"string\"}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "B9RWxD1btNvK" - }, - "source": [ - "Now we are ready to use BigQuery DataFrames!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "wJ0gXezj2w1t" - }, - "source": [ - "## Visualization #1: Cases over time in the US" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "xckgWno6ouHY" - }, - "source": [ - "### Set up project and filter data" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "-uiY0hh4tNvK" - }, - "source": [ - "First, let's do project setup. We use options to tell BigQuery DataFrames what project and what region to use for our cloud computing." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "id": "R7STCS8xB5d2" - }, - "outputs": [], - "source": [ - "import bigframes.pandas as bpd\n", - "\n", - "# Note: The project option is not required in all environments.\n", - "# On BigQuery Studio, the project ID is automatically detected.\n", - "bpd.options.bigquery.project = PROJECT_ID\n", - "\n", - "# Note: The location option is not required.\n", - "# It defaults to the location of the first table or query\n", - "# passed to read_gbq(). For APIs where a location can't be\n", - "# auto-detected, the location defaults to the \"US\" location.\n", - "bpd.options.bigquery.location = REGION\n", - "# Improves performance by avoiding generating total row ordering\n", - "bpd.options.bigquery.ordering_mode = \"partial\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "v6FGschEowht" - }, - "source": [ - "Next, we read the data from a publicly available BigQuery dataset. This will take ~1 minute." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "zDSwoBo1CU3G" - }, - "outputs": [], - "source": [ - "all_data = bpd.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9qV2y3iHp13y" - }, - "source": [ - "Using pandas syntax, we will select from our all_data input dataframe only those rows where the country_code is US. This is called row filtering." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "UjMT_qhjf8Fu" - }, - "outputs": [], - "source": [ - "usa_data = all_data[all_data[\"country_code\"] == \"US\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "IYCUayWkwq8c" - }, - "source": [ - "We're only concerned with the date and the total number of confirmed cases for now, so select just those two columns as well." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "IaoUf57ZwrJ8" - }, - "outputs": [], - "source": [ - "usa_data = usa_data[[\"date\", \"new_confirmed\"]]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "94oqNRnDvGkr" - }, - "source": [ - "### Sum data" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "TNCQWZW83U0b" - }, - "source": [ - "`usa_data.groupby(\"date\")` will give us a groupby object that lets us perform operations on groups of rows with the same date. We call sum on that object to get the sum for each day. This process might be familiar to pandas users." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "tYDoaKgJChiq" - }, - "outputs": [], - "source": [ - "# numeric_only = True because we don't want to sum dates\n", - "new_cases_usa = usa_data.groupby(\"date\").sum(numeric_only = True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "3jcwFPgK5BLh" - }, - "source": [ - "### Line graph" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8GvJAgnH5Nzi" - }, - "source": [ - "BigQuery DataFrames implements some plotting methods with the matplotlib backend. Use `DataFrame.plot.line()` to draw a simple line graph." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "gFbCgfFC2gHw" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "new_cases_usa.plot.line(\n", - " rot=45,\n", - " ylabel=\"New Cases\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "sM5-HFDx70RG" - }, - "source": [ - "## Visualization #2: Symptom-related searches compared to new cases" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "se1b6Vf4XB9_" - }, - "source": [ - "### Filter data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Wl2o-NYMoygb" - }, - "source": [ - "We're curious if searches for symptoms like \"cough\" and \"fever\" went up in the same times and places that new COVID-19 cases occured, compared to non-symptoms like \"bruise.\" Let's plot searches vs. new cases to see if it looks like there's a correlation." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "olfnCzyg8jYi" - }, - "source": [ - "First, we select the new cases column and the search trends we're interested in." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "metadata": { - "id": "LqqHzjty8jk0" - }, - "outputs": [], - "source": [ - "regional_data = all_data[all_data[\"aggregation_level\"] == 1] # get only region level data,\n", - "symptom_data = regional_data[[\"location_key\", \"new_confirmed\", \"search_trends_cough\", \"search_trends_fever\", \"search_trends_bruise\", \"population\", \"date\"]]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "b3DlJX-k9SPk" - }, - "source": [ - "Not all rows have data for all of these columns, so let's select only the rows that do. Finally, lets add a new column capturing new confirmed cases as a percentage of area population." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "g4MeM8Oe9Q6X" - }, - "outputs": [], - "source": [ - "symptom_data = symptom_data.dropna()\n", - "symptom_data = symptom_data[symptom_data[\"new_confirmed\"] > 0]\n", - "symptom_data[\"new_cases_percent_of_pop\"] = (symptom_data[\"new_confirmed\"] / symptom_data[\"population\"]) * 100\n", - "\n", - "\n", - "# remove impossible data points\n", - "symptom_data = symptom_data[(symptom_data[\"new_cases_percent_of_pop\"] >= 0)]\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# group data up by week\n", - "weekly_data = symptom_data.groupby([symptom_data.location_key, symptom_data.date.dt.isocalendar().week]).agg({\"new_cases_percent_of_pop\": \"sum\", \"search_trends_cough\": \"mean\", \"search_trends_fever\": \"mean\", \"search_trends_bruise\": \"mean\"})" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "IlXt__om9QYI" - }, - "source": [ - "We want to use a line of best fit to make the correlation stand out. Matplotlib does not include a feature for lines of best fit, but seaborn, which is built on matplotlib, does.\n", - "\n", - "BigQuery DataFrames does not currently integrate with seaborn by default. So we will demonstrate how to downsample and download a DataFrame, and use seaborn on the downloaded data." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "T9Hub_EAXWvY" - }, - "source": [ - "### Graph with lines of best fit" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "id": "hoQ9TPgUPJnN" - }, - "source": [ - "We will now use seaborn to make the plots with the lines of best fit for cough, fever, and bruise. Note that since we're working with a local pandas dataframe, you could use any other Python library or technique you're familiar with, but we'll stick to seaborn for this notebook.\n", - "\n", - "Seaborn will take a few minutes to calculate the lines. Since cough and fever are symptoms of COVID-19, but bruising isn't, we expect the slope of the line of best fit to be positive in the first two graphs, but not the third, indicating that there is a correlation between new COVID-19 cases and cough- and fever-related searches." - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "metadata": { - "id": "EG7qM3R18bOb" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 59, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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N9z+2hBcuapN1XaZLoaoctRsO1O0dq100O4Avdnjder0yd2SMlQ8P+ClqSIBtDX4B2HI967neeriJC9OldZd74przSbSbKL9S+a2girLJA2FdEzb954SgUCwc9ouPt72trfxB3/wB7z22mvcd999vPDCC3zrW9/i937v91p+v23b2PYb463lcrndRwI2Tvk3REk2oXFjqUYmruIH4Z72D6zMCOSqNrmas0oknehLNk3PwjAkaahULLf5s5IksVixuZWrM1+2mS/bnOrf2JW1OS3iB4RhNJlwrDdJX0rfs+bT3WY3p0/2S9hYro8XBAxkYqRiats2zTpewFcvz/P0pSlu5errro91x/n4o2P82AODGNre91FIkkRSV5qvWSAQHHza/pv8a7/2a5TLZe6//34URcH3fX77t3+bn/mZn2n5/Z/61Kf4rd/6rXYfYx19KZ2RrlhzqqUvFW3qbIiSXNVBlSWm8iYxXSJpKIRhuGEJpp0BaOUdfMpQKZneKpEkSVLT9GyjhWqNczx+opebS9VNXVkbgmu0O7G85dZoBuatZiY2e/07fW/a+Z7u5vTJXo/VrvXqaNem2arl8cUXZvj8c9Pkas666w8MZ3jysXHedqp3X8oaoqwiEBxe2i4+nn76af77f//v/I//8T948MEHef755/nlX/5lRkZG+Lmf+7l13//JT36ST3ziE80/l8tlxsfH230sFis2l2fLVG2PpapNb1JnMBtvZh0qlstod5ybSzVML+C713NMdCc3LMHsVgBa2xfRl9JZKFvNP1cst2VQTRkqqiJjuT6j3ZHvxVZNzJK6suo5thL0N3v9d+qv2eh52vme7ub0yV6N1Xp+QKHuNrNc7WK+bPG5Z6f40otzmO56x9O3n+rlycfGeWj07qfAtosqy6RikeAQZRWB4PDSdvHxK7/yK/zar/0aP/3TPw3A2bNnuXXrFp/61Kdaig/DMDCM3U/v387XeX2xRldcY75cY6Insco+XZIkDFWmL20QhiG3l2qYKZdcNWxasa9kJ6OsWwnqa/sY1mY6RrpiLYPqdpo5135vEAR88+oSNdsjaai8896+O1rLb/b679Rfs5G4aGdQ383pk90eq/WDaGy2ZLpt9eq4tlDl6YuT/NWVhXVNpJoi8WMPDPHxC2PNnUZbJQxD8jWXuuM1S0HbyViJsopAcPRo+296vV5H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ueOOO3j00Ue7Hdaa+exnP4umaXz4wx/udigd8clPfhJN01Z87dixo9thddSRI0f4gz/4A8rlMq7rcsMNN/D00093O6yO2bRp0yl/h5qmsXv37m6HdtGSJOEv//Iv2bx5M67rsnXrVv7qr/7qvM5fWUvrIvn4l3/5Fz7ykY/wwAMP8Oyzz3LjjTfyG7/xG0xPT3c7tI7wPI8bb7yRL33pS90OZU08/vjj7N69m6eeeorHHnuMKIp4z3veg+d53Q6tI8bGxvjsZz/LM888w9NPP8073/lOfuu3fotf/epX3Q6t4/bs2cNXvvIVdu3a1e1QOur666/n2LFjy19PPvlkt0PqmIWFBd7ylrdgWRaPPvooL7zwAn/7t39Lb29vt0PrmD179qz4+3vssccAeN/73tflyC7e5z73Ob785S/zxS9+kRdffJHPfe5z/M3f/A1f+MIXuhuYWgduu+02tXv37uXvkyRRo6Oj6sEHH+xiVGsDUI888ki3w1hT09PTClCPP/54t0NZM729veof//Efux1GR9XrdXXttdeqxx57TL3jHe9Q999/f7dD6ogHHnhA3Xjjjd0OY8189KMfVW9961u7HcYldf/996utW7eqNE27HcpFu/vuu9V999234rHf+Z3fUffcc0+XImq76mc+wjDkmWee4d3vfvfyY7qu8+53v5v//d//7WJk4kJVq1UA+vr6uhxJ5yVJwje/+U08z+OOO+7odjgdtXv3bu6+++4V/y9eLV599VVGR0fZsmUL99xzDwcPHux2SB3z7//+79xyyy28733vY3BwkJtuuomvfe1r3Q5rzYRhyD//8z9z3333dfxw02648847+eEPf8grr7wCwC9+8QuefPJJ7rrrrq7GddkdLNdps7OzJEnC0NDQiseHhoZ46aWXuhSVuFBpmvLhD3+Yt7zlLezcubPb4XTMc889xx133IHv++TzeR555BHe9KY3dTusjvnmN7/Js88+y549e7odSsfdfvvtfP3rX2f79u0cO3aMT33qU7ztbW/j+eefp1AodDu8i7Z//36+/OUv85GPfIRPfOIT7Nmzhz/5kz/Btm3uvffebofXcd/5zneoVCp84AMf6HYoHfGxj32MWq3Gjh07MAyDJEn49Kc/zT333NPVuK765ENcXXbv3s3zzz9/Va2pA2zfvp29e/dSrVb513/9V+69914ef/zxqyIBOXToEPfffz+PPfYYmUym2+F03Il3kLt27eL2229nYmKCb33rW/zhH/5hFyPrjDRNueWWW/jMZz4DwE033cTzzz/PP/zDP1yVycc//dM/cddddzE6OtrtUDriW9/6Ft/4xjd4+OGHuf7669m7dy8f/vCHGR0d7erf31WffPT392MYBlNTUysen5qaYnh4uEtRiQvxoQ99iO9973s88cQTjI2NdTucjrJtm2uuuQaAN7/5zezZs4e///u/5ytf+UqXI7t4zzzzDNPT09x8883LjyVJwhNPPMEXv/hFgiDAMIwuRthZPT09bNu2jddee63boXTEyMjIKUnwddddx7/92791KaK1c+DAAX7wgx/w7W9/u9uhdMyf//mf87GPfYzf+73fA+CGG27gwIEDPPjgg11NPq76mg/btnnzm9/MD3/4w+XH0jTlhz/84VW3pn61UkrxoQ99iEceeYT//u//ZvPmzd0Oac2laUoQBN0OoyPe9a538dxzz7F3797lr1tuuYV77rmHvXv3XlWJB0Cj0WDfvn2MjIx0O5SOeMtb3nLK1vZXXnmFiYmJLkW0dh566CEGBwe5++67ux1KxzSbTXR95VBvGAZpmnYporarfuYD4CMf+Qj33nsvt9xyC7fddhuf//zn8TyPD37wg90OrSMajcaKu6zXX3+dvXv30tfXx/j4eBcj64zdu3fz8MMP893vfpdCocDk5CQApVIJ13W7HN3F+/jHP85dd93F+Pg49Xqdhx9+mB//+Md8//vf73ZoHVEoFE6pz8nlcpTL5auibufP/uzPeO9738vExARHjx7lgQcewDAMfv/3f7/boXXEn/7pn3LnnXfymc98ht/93d/lZz/7GV/96lf56le/2u3QOipNUx566CHuvfdeTPPqGRrf+9738ulPf5rx8XGuv/56fv7zn/N3f/d33Hfffd0NrKt7bS6hL3zhC2p8fFzZtq1uu+029dRTT3U7pI750Y9+pIBTvu69995uh9YRp3tvgHrooYe6HVpH3HfffWpiYkLZtq0GBgbUu971LvVf//Vf3Q5rTV1NW23f//73q5GREWXbttqwYYN6//vfr1577bVuh9VR//Ef/6F27typHMdRO3bsUF/96le7HVLHff/731eAevnll7sdSkfVajV1//33q/HxcZXJZNSWLVvUX/zFX6ggCLoal6ZUl9ucCSGEEGJdueprPoQQQghxeZHkQwghhBCXlCQfQgghhLikJPkQQgghxCUlyYcQQgghLilJPoQQQghxSUnyIYQQQohLSpIPIYQQQlxSknwIIYQQ4pKS5EMIIYQQl5QkH0IIIYS4pCT5EEIIIcQl9f8DvBq4eqmKlScAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import seaborn as sns\n", - "\n", - "# draw the graph. This might take ~30 seconds.\n", - "sns.regplot(x=\"new_cases_percent_of_pop\", y=\"search_trends_cough\", data=weekly_data, scatter_kws={'alpha': 0.2, \"s\" :5})" - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "metadata": { - "id": "5nVy61rEGaM4" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 62, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGeCAYAAAA0WWMxAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAArzVJREFUeJzs/XmMnOl1349+3r32qt437rORM+SMJMvWYkmWYtkaymtyE8O5RqDYQBLAAWxHQGwrsA07sK04fxhGcgM7zgWcBNkQ3Fz7l5ufZrxKtmRLsuSRNMMZkjMckk2y96qu/d3f97l/vFU13c3qjey9ng9Ay+yu7nq6OF3n+5zzPecoQgiBRCKRSCQSyQGhHvYBJBKJRCKRDBZSfEgkEolEIjlQpPiQSCQSiURyoEjxIZFIJBKJ5ECR4kMikUgkEsmBIsWHRCKRSCSSA0WKD4lEIpFIJAeKFB8SiUQikUgOFCk+JBKJRCKRHCj6YR9gI3EcMz8/Tz6fR1GUwz6ORCKRSCSSHSCEoNlsMj09japuk9sQu+TP//zPxfd///eLqakpAYjf//3f733O933xsz/7s+Ly5csik8mIqakp8Q/+wT8Qc3NzO/7+9+/fF4D8I//IP/KP/CP/yD/H8M/9+/e3jfW7zny0221eeOEFfuInfoK/83f+zrrP2bbNK6+8wi/+4i/ywgsvUK1W+emf/ml+8Ad/kK9//es7+v75fB6A+/fvUygUdns8iUQikUgkh0Cj0eD06dO9OL4VyuMsllMUhd///d/nh3/4hzd9zNe+9jW+4zu+g9nZWc6cObPt92w0GhSLRer1uhQfEolEIpEcE3YTv/fd81Gv11EUhVKp1PfznufheV7v741GY7+PJJFIJBKJ5BDZ124X13X5uZ/7Of7+3//7m6qgz372sxSLxd6f06dP7+eRJBKJRCKRHDL7Jj6CIOBHfuRHEELw27/925s+7jOf+Qz1er335/79+/t1JIlEIpFIJEeAfSm7dIXH7Owsf/Znf7Zl7ceyLCzL2o9jSCQSiUQiOYLsufjoCo+33nqLz3/+84yMjOz1U0gkEolEIjnG7Fp8tFotbt261fv7nTt3+OY3v8nw8DBTU1P83b/7d3nllVf4P//n/xBFEYuLiwAMDw9jmubenVwikUgkEsmxZNettl/4whf42Mc+9tDHP/WpT/HLv/zLnD9/vu/Xff7zn+ejH/3ott9fttpKJBKJRHL82NdW249+9KNspVceY2yIRCKRSCSSAUAulpNIJBKJRHKgSPEhkUgkEonkQJHiQyKRSCQSyYGy7+PVJZKtEEKw0vRoeSE5S2csb6EoymEfSyKRSCT7iBQfkkNlpenx6oM6USzQVIXnTxUZL6QO+1gSiUQi2Udk2UVyqLS8kCgWTJfSRLGg5YWHfSSJRCKR7DNSfEgOlZylo6kK8zUHTVXIWTIZJ5FIJCcd+U4vOVTG8hbPnyqu83xIJBKJ5GQjxYfkUFEUhfFCivHDPohEIpFIDgxZdpFIJBKJRHKgSPEhkUgkEonkQJHiQyKRSCQSyYEixYdEIpFIJJIDRYoPiUQikUgkB4oUHxKJRCKRSA4UKT4kEolEIpEcKHLOh2RfkAvjJBKJRLIZUnxI9gW5ME4ikUgkmyHLLpJ9QS6Mk0gkEslmSPEh2RfkwjiJRCKRbIaMCJJ9QS6Mk0gkEslmSPEh2RfkwjiJRCKRbIYsu0gkEolEIjlQpPiQSCQSiURyoEjxIZFIJBKJ5ECRno8TiBzwJZFIJJKjjBQfJxA54EsikUgkRxlZdjmByAFfEolEIjnKSPFxApEDviQSiURylJFR6QQiB3xJJBKJ5CgjxccJRA74kkgkEslRRpZdJBKJRCKRHCgy8yGR7AGyvVkikUh2jhQfEskeINubJRKJZOfIsovkSCOEYLnhcnulxXLDRQhx2Efqi2xvlkgkkp0jMx+SI81xySjI9maJRCLZOfIdUnKkWZtRmK85tLzwSHbxyPZmiUQi2TlSfEiONMcloyDbmyUSiWTnHM13comkg8woSCQSyclDig/JkUZmFCQSieTkIbtdJBKJRCKRHChSfEgkEolEIjlQBr7sIidTSiQSiURysAy8+DgucyQkEolEIjkpDHzZRU6mlEgkEonkYBl48XFc5khIJBKJRHJSGPhIK+dISCQSiURysAy8+JBzJCQSiUQiOVgGvuwikUgkEonkYJHiQyKRSCQSyYEixYdEIpFIJJIDRYoPiUQikUgkB4oUHxKJRCKRSA4UKT4kEolEIpEcKFJ8SCQSiUQiOVCk+JBIJBKJRHKgSPEhkUgkEonkQNm1+PiLv/gLfuAHfoDp6WkUReEP/uAP1n1eCMEv/dIvMTU1RTqd5uMf/zhvvfXWXp332CKEYLnhcnulxXLDRQhx2EeSSCQSieRQ2LX4aLfbvPDCC/y7f/fv+n7+X//rf82/+Tf/ht/5nd/hq1/9Ktlslk984hO4rvvYhz3OrDQ9Xn1Q562lFq8+qLPS9A77SBKJRCKRHAq73u1y9epVrl692vdzQgh+67d+i1/4hV/gh37ohwD4z//5PzMxMcEf/MEf8KM/+qOPd9pjTMsLiWLBdCnNfM2h5YVyn4xEIpFIBpI99XzcuXOHxcVFPv7xj/c+ViwWed/73seXv/zlvl/jeR6NRmPdn5OGEAI3iCi3PN6Yr6OpkLMGfqefRCKRSAaUPRUfi4uLAExMTKz7+MTERO9zG/nsZz9LsVjs/Tl9+vReHulIsNL0mKs6GKpKEMVMl9KM5a3DPtaJRnpsJBKJ5Ohy6N0un/nMZ6jX670/9+/fP+wj7TktLyQWcGm6wFg+RcrQUBTlsI91LHhUESE9NhKJRHJ02VPxMTk5CcDS0tK6jy8tLfU+txHLsigUCuv+nDRylo6mKszXHDRVkSWXXfCoImKtxyaKBS0v3OeTSiQSiWSn7Kn4OH/+PJOTk/zpn/5p72ONRoOvfvWrfOADH9jLpzpWjOUtnj9V5KmJHM+fKsqSyy54VBEhBZ9EIpEcXXb9jtxqtbh161bv73fu3OGb3/wmw8PDnDlzhp/5mZ/hV3/1V3nqqac4f/48v/iLv8j09DQ//MM/vJfnPlYoisJ4ISW7Wx6BRxURXcHX8kJyli4Fn0QikRwhdi0+vv71r/Oxj32s9/dPf/rTAHzqU5/iP/7H/8jP/uzP0m63+cf/+B9Tq9X40Ic+xMsvv0wqldq7U0sGhkcVEVLwSSQSydFFEUesDaDRaFAsFqnX6yfS/yGRSCQSyWEgRFK6doKI8fzeJwR2E79lIVwikUgkkhNMHAsabkDDCQnjGFM/9EZXKT4kEolEIjmJhFFM3QlouiHx0SpySPEhkUgkEslJwgsj6k5A24uO7IBFKT5I6mArTW+dqVEOAZNIJBLJccLxI2qOj+NHh32UbZHig3cGWUWxQFMVnj9VZLwgu3MGHSlKJRLJUadrIq07AX4YH/ZxdowUH8iNs5L+SFEqkUiOKnEsaLqJ6Ajj4yM6ukjxgZyGKemPFKUSieSoEUYxDTek4QRHzkS6G2SURU7DlPRHilKJRHJU8MOYmuMfaRPpbpDvpshpmJL+SFEqkUgOG8dPOlds/2Qtx5TiQyLZBClKJRLJYdE1kXrB0e9ceRSk+JBIJBKJ5AgQx4Kml/g5guj4mUh3gxQfEolEIpEcIlEsOpNIA6L4+Ps5doIUHxKJRCKRHAJ+mIw/b3nhiTCR7gYpPiQSiUQiOUDcoDv+/GSZSHeDFB+PwHGffHkUzn8UziCRSCQHSdsLqZ1gE+lukOLjETjuky+PwvmPwhkkEolkvxFC9IaCnXQT6W5QD/sAh40QguWGy+2VFssNd0d1t7WTL6M4mat/nDgK5z8KZ5BIJJL9IooF1bbPvVWbSsuTwmMDA5/5WGl6fOt+jWo7wI8i3nN2iEtThS1LAEdh8uXjlC2Owvl3cgZZmpFIJMeNIIo7nSuDZyLdDQMvPlpeSLUd0PQCVpoeiqIwmrO2LAEchcmXj1O2OArn38kZZGlGIpEcF6SJdHcMvPjIWTp+FLHS9BjNW+iqsu0CsaMw+fJxlp4dhfPv5AxysZtEIjnqtDuTSF1pIt0VAy8+xvIW7zk7hKIo6KrCSM48FgvEsqZGywt45Z5DztLJmtphH2nPOQrlIYlEItmIEMkk0rotTaSPysC/myuKwqWpAqM569gtEBMCEJ3/PYEchfKQRCKRdIliQdMNqDuDM4l0vxh48QFHowyxW9p+RD5l8MxkgfmaQ9s/eSm/4/jvIpFITh5dE2nLDYlP6m3vgJHi45giSxISiUSyv7hBRKMz/lyyt8iIdUyRJQmJRCLZH2w/MZE6JzCjfFSQ4uOYIksSEolEsncIkQw7rEkT6YEgxYdEIpFIBpY4FjTcgIYTEsaDIzqEEIc6tFGKD4lEIpEMHOGaSaSDZCK9U27zR68v8vpCg//rn34ITT0cASLFh0QikUgGBi/sTiKNBmb8ecsL+fyNZV66tsiNxWbv4198a4WPPnM4xXspPiQSiURy4nH8iJrjD4yJVAjBqw/qfO7aIn/x5gpe+HBJ6X+9MifFh2TvOazFbHIhnEQiOQp0TaR1J8DvE3xPIitNjz98fZGXX19kvub2fczFyTw/9v6z/OAL0wd8uncYePHRL1ACJyJ4HtZiNrkQTiKRHCZxLGi6iegYBBOpH8Z8+XaFl64t8vW7q/QbvlpI6Xz82QmuXp7k0lSBU0OZgz/oGgZefPQLlMCxCZ5bZRkOazGbXAgnkUgOgzCKabghDScYCBPp7ZUWL11b5I/fWKLhPjwITQG+/dwQL16e4oNPjGDq6sEfchMGXnz0C5TAsQmeW2UZDmsK6mFOX5UlH4lk8BgkE2nLDfmzm4l59OYa8+hapoopXrw8ySeenTiyF+eBFx+bBcrjMrp8qyzDYU1B3fi8ozmT5YZ7IIJAlnwkksHB8RPRYfsne/x5LATful/jpWuL/MVb5b7+FVNX+chTo1y9PMkLp0uoR/zSdXSj6gGxWYDu97GjeKveKstwWFNQNz7vcsPtCQJVgZmhNClD25fXUJZ8JJKTT9dE6gUnu3Nlpenx8uuLvHxtkYV6f/PoMxN5Xrw8yXdfHCeXOj4h/ficdJ/YLED3+9hh3aq3Ej3HYcfLWkFwfb7BctNjNGfty2soF+5JJCeTOBY0vcTPcZLHn/fMo68t8PXZ6qbm0e95doIXL0/yxFju4A+5B8h35g1sZ+AMo5i0qXO33KKYPpjsx1ai5zCyG7vNAK0VBH4UYWjqvmUmjoMYk0gkO6drIm26AVG/SHxCuL3S4nPXFvmTY2gefRSk+NjAdgbOlhfy6lw9+fuqzdmR7L5nP45aKWG3GaC1guD0cPIz7FdmQi7ck0hOBn6YjD9veeGJNZH2zKOvLXJzaXPz6NXLk3zvETaPPgpSfGxgOwPn2ZEMbT/k3EgWJ4gORAgcVClhpxmN3YqhtYJACMFozpKZCYlE0hc3iKjZJ9dEuhPzqKWrfOTpMV58buJYmEcfBSk+NrCdgfPsSJa6E+IGMbqqHoinYK9LCZuJjJ1mNB5HDMnMhEQi6UfbC6mdYBPpcsPlD19f4uXXNzePXpxMzKN/6+L4iferneyfbgdsDMSjOXPLQH8YnoK9DtibiYydZjSkr0IikewFQojeULCTaCL1w5i/ervcmTxapV/xqGsevXp5kgvH1Dz6KAy8+NgsEG8W6E/Czb3pBlRaHsWMQaXl03QDxgupHWc0TsJrIJFIDo8oFjScgMYJNZG+vdLipdcW+ZPr/c2jqgLvPTfMJy9P8oEnRjC0420efRQGXnwcNTPnQeCFMQ+qDnfKbQxN5UpnpLzMaEgkkv0kiGJq9sk0kbbckD+9scxL1xZ4c6nV9zHTpa55dHLg318HXnx0b/tzNZu2F1JpeUdmgNh+Yekqp4cyFNI6DSfE6rRsyYyGRCLZD9ygO/78ZJlIYyH45v0aL+/APPrJy5NcOVU8kebRR2HgxUf3tj9badNyQyotn7oTcmWmgKIoR2qa6V6RTxkM50yiWDCcM8mnjMM+kkQiOYG0O5NI3RNmIl1quPzRDsyjn7wyyUefOfnm0Udh4F+R7m2/5YWstoNe+eXeqk3dCbft/NjNwK2jMp5dllckEsl+IUQyibRunywT6U7Mo8W0wfd2Jo+eH80e+BmPEwMvPrpsNFvCzjbb7mbg1lFZenbSyitHRdRJJIPMSTWRvr2cTB790y3Mo99xfpgXL0/ygQuDaR59FKT46LAxGyCEoO40tu382I1hdRDNrQfBURF1EskgEkTJJNKme3JMpE034M9uLPO51xZ5a7m/eXSmlObq5Um+59kJmT1+BAZWfPS7LY8XUoyt+fh0KYWlq+RTxqb/ce1m4JZcerY/SFEnkRw8bhDR6Iw/PwnEQvDNezU+d22RL761QhA9LKRSHfPo1SuTPD9TlBnWx2Bgo99mt+XH2Vuy1j/RT9xIr8X+IEWdRHJw2H5IzT45JtLFhssfXlvk5dcXWWp4fR9zaSrP1ctTfOyZMbLy/WVPGNhXsXtbniqmuLHQ5PpCA6C3OXG7W/RGcXF+NLtOBW86vOwEeS2OClLUSST7ixCClpeIjpNgIvXDmL+8VeZz1xZ5Zba/ebSUNvj4s+NcvTwlzaP7wMCKj+5t+cZCk/tVG4EgiATTpdSObtHbZUhkKeDgOGkGWonkqBDHgoYb0HBCwvj4i463lpq8dG2RP72xTFOaRw+VgRUf3dvy9YUGAsGl6QILNRdLV3d0i95OXOxVKUB2ckgkkoMmXGMijY+5ibThBJ3Jo4vc2sY8+r3PTTCak5nTg2BgxUf3tgwQRIKFmoumKuRTxo5u0duJi70qBchODolEclB4YUTdDmj70bHuXImF4JXZKi9dW+RLt8qbmke/65kxXrwszaOHwcCKjy79RMJOsg3biYu9KgXI8o1EItlvbD+ZROr4x9tEuthwefnaIi9fW2S52d88+mzHPPpRaR49VAb+le8nEpYb7rbZhoPyGchODolEsh90TaR1J+i7k+S44IcxX3yrzMvXFnjlXm1T8+j3PDvB1SuTnBuR5tGjwJ5HsiiK+OVf/mX+y3/5LywuLjI9Pc0//If/kF/4hV84Nmmto5RtOKqdHNKLIpEcT+JY0HQT0XGcTaRvLTU7k0eX+84aURV43/kRrl6e5P0XhtGlebTHUXiv3nPx8Ru/8Rv89m//Nv/pP/0nnnvuOb7+9a/z4z/+4xSLRX7qp35qr59uz1gbTN0gQlXYVbZhv4LxUe3kkF4UieR4cRJMpA0n4E+uL/PytUVurfQ3j54aSvPic9I8uhFVUchYGhlTJ2Noh32cvRcff/VXf8UP/dAP8X3f930AnDt3jv/+3/87f/3Xf73XT7WnrA+mMDOUJmVoO842DFowPkrZIYlEsjle2F1nfzxNpLsxj169PMkVaR7toasqGUsja+qkDPVIvS57Lj4++MEP8ru/+7u8+eabPP3003zrW9/iS1/6Er/5m7/Z9/Ge5+F57xiDGo3GXh9pR2wMpilD48JY7pG//qQH48SLAm/M1wljwenhNEKII/Uft0QyyDh+Ijps/3iOP1+su7z8+tbm0eemC1y9PMlHnxkjY0o/HIChqWQtnYypkToCGY7N2PN/rZ//+Z+n0Whw8eJFNE0jiiJ+7dd+jR/7sR/r+/jPfvaz/Mqv/MpeH2PXPK6xc9CMoWN5i+lSmsW6i6lpzFUdRnPWic72SCRHHSEEbT+iZvvH0kS6E/PoUOadtfVnpXkUgJSRZDcylnZsBqPteYT8n//zf/Jf/+t/5b/9t//Gc889xze/+U1+5md+hunpaT71qU899PjPfOYzfPrTn+79vdFocPr06b0+1pYIIRBCUEwnL8eZ4cyWO1r63e6PqjF0v1AUhZShMZZPPVa2Z+PrO5ozKbd8aWSVSHZB10TacI/f+HMhBG8tt5LJo9uYRz95ZZL3nZfmUUVRSBtar6SiqcfvPXLPxcc//+f/nJ//+Z/nR3/0RwG4cuUKs7OzfPazn+0rPizLwrION1CvND1em2v0/BqKovQC3kn3cjyOUXYvsj0bX9/pUor5mnskX2/Z4SM5aoRRTMMNezupjhN1J+BPry/z0rUF3l5p933MqaHO5NFnJxgZcPOopiqkzURspA0N9RgKjrXsufiwbRtVXa9KNU0jPqItXUIIZitt5mo250ayOEG07ga/Uy/HcRUpj3Puvcj2bHx9V5rekfXOHNd/Y8nJww+TzpWWFx4rE2kUC165V+Wl1xb5y7c3MY8aKh99epxPXpnkuenCQAv8o2wYfVz2XHz8wA/8AL/2a7/GmTNneO655/jGN77Bb/7mb/ITP/ETe/1Ue8JK02O2YrPU8FhqeDwxll13g9/p7f64Gk4f59x70Qa88fUdy1vM19wj6Z05rv/GkpODG0TU7ONnIl2oO/zhtSVefn1r8+gnL0/yXQNuHj0uhtHHZc//hf/tv/23/OIv/iI/+ZM/yfLyMtPT0/yTf/JP+KVf+qW9fqo9oXtrf9/5Ee6WW+v8HrDz2/1uShBHKX2/2bkP6owbX9/RnMlozjqS3plBMxVLjg7dSaRecHzGn3tBxJc6a+u/ca/W9zFd8+jVy1OcGckc7AGPEJahkTWTGRymPhh+FkUcsZxdo9GgWCxSr9cpFAr7/nw7GaW+E3YTrPfqOfeCzc692RmPknA6aAb5Z5ccPEIIGm5Iwzk+JtKeefS1ZG39ZubR919IJo8Osnk03REbWVM7Ma/BbuL3wF/d9qpLZTcliL1K3+9FMNzs3JudcZB9D0d12qzkZBHFgoYT0DhGJtLEPLrES9cWNzWPnu6aR5+bZDhrHvAJD59uh0q2M2X0OHao7CUDIz42C9SHEVD2Kn2/n0JgszPut+9BZhckg8pxM5F2zaOfe22Rv9rCPPqxZ8a5enkwzaOqopAxNTJWMtL8uHeo7CUDIz6O0o19r7It+ykENjvjfvsejtK/k0RyELhBd/z58TCRztccXn59kT+8tsRKq7959PJ0gatXpvjo02OkzZNrmuyHrqqkzWQ1x0nrUNlLBkZ8NN2A1ZZPIa2z2gpousGhBbW9yrbspxDY7Iz7PUxNdpRIBoV2x0TqHgMTqRdEfPFWmc+9tsg379f6PmYoY/CJ5yZ58fIkZ4YHyzxqaCoZUyNr6Se6Q2UvGRjx4YUx96s2QTnG0FQun0rMMMc5zX8YU1X3u0y1naDa6t/rOP9bSgYDIQRNL6RuH30TqRCCN5dafO7aAn92Y5m297BIUhX4wIURXhxA86ipq72R5pYuBcduGRjxYekqp4bSFDMGdTvA6rQzHec0/0k0QG4nqLb69zrO/5aSk81xMpHW7YA/ubHES68tcrvc3zx6ZjjDi53Jo4NkHj2OO1SOKgMjPvIpg5GcRRQLRnIW+ZQBrE/zz1VtZittmm6AF8ZYuko+ZezJDXq7W3m/zwNH/ibfPfdevWbbCaqtyjKyZCM5agRRYiJtukfbRBrFgr+ZrfK5awv81a0KYR+BlDY0PnYxWVv/7NRgmEdPwg6Vo8rAiI+dGChbXkjbD7m90uZB1eH0UIbhnLknN+jtbuX9Pg8c+Zt899yVlrfmNTOYLqVJGdqei6atyjJyCJjkqOAGEY1O58pRZifm0SszBV68PDjmUdmhcjAMzLvzTgyUlZZHpe0jhKA+5zOaNVhtsSfm1O1u5f0+DxzKTX433onuuYsZgzvlNoW0TqXls1h3Gcun9lw0bVWWGbTNwpKjh+2H1OyjbSJ1g4gvvlXmpWubm0eHs2Zvbf0gmEc1VUkGflkaaUMbiKzOYTMw4mMz1oqSnKVTd0LuVNqs2j5iBUoZs2dO3Q0bA3jW1La8la+/tSdvEG0vpOUFzNUEuqoe2E1+N96J7rkrLR9DU2k4IWEsMDVtX0TTVmWZk+iBkRx9joOJVAjBzaUmL11b5M+uL9P2HxZHmqrw/gvDncmjIye+xCA7VA6XgRcfa+nenHUViAWnhtM0nLBnTt0NGwP4lZnClrfytbd2N4iYqzpEsUAIGMmanB3JHthNfjfeie65m27AlVNFLF3FC2Pmqo4sfzwCsmPn+BDHgoYbdAT30RQddTvgj68v8fK1rc2jVy9P8j0DYB6VHSpHBxkV1tC9OQMEkaDaTm4yXhgjhNhVENgYwNt+xIWx3KZBfO2t/fZKi1jAzFCG+ZrDSM7a930za9mNd6J37jXnE0Ic2eVwRx3ZsXP0CdeYSOMjaCKNYsHXZ1d56dri1ubRZ8a4euXkm0dlh8rRRIoPHg7SozmTmaE0y00PQ1OZrzmM7lIAPI75cS/Hr3/rfo1qO8CPIt5zdohLO3ijeVzvxH6XP05ydkB27BxdvDCibge0/ehIdq7M1RxevrbIH76+SLnl933MlZkiVztr69MntNQgO1SOB1J80P+2mTI0RnPWuiAwtoug9zgBfC/Hr1fbAU0vYKXpoSjKjkTUUfdOnOTsgOzYOXrYfjKJ1Onjkzhs3CDiL94q8/K1Bb55v973MSNZk+95doKrlyc5fULNo7JD5fgh39nof9vsFwR2E/QeJ4Dv5fh1P4pYaXqM5i10VTkRN+mTnB2QHTtHAyEErc74cz88Wn4OIQQ3FhPz6OdvbG4e/cCFET55ZZJvPzd8Im//skPleCPFB93bJrw+V6PqBCiK4PmZIldmCrT9qBcE7pTbxyrojeUt3nN2CEVR0FWFkZx5Im7SJzk7cNSzTiedOBY03UR0HDUTac32+ePry7z02gJ3K3bfx5wdznD1SmIeHcqcPPOo7FA5OZycd+3HYCyflFfeWmqy1PBpdsxkH35qjAtjud7j9iroHZRnQVEULk0VtjV/HjcPhcwOSPaao2oijWLB1+4m5tEvv93fPJoxNT76zBifvDzFpan8kf7dfRRkh8rJRIoPkiCdMjQyps50SQOSlOvGzMZeBb2D9Czs5CZ93DwUMjsg2Su8sLvO/miZSLvm0ZdfX6SyiXn0+VOJefQjT58886jsUDn5SPHRIWfpZC2dpWbSC/9ELvtQZmOvgt5R8yzs13mOW0ZFMjg4fiI6bP/ojD93g4i/eHOFl64t8q0Hm5tHP/FcMnn01NDJMY+u7VDJGNpAbccdVKT4IAmSQgjODKcppHSK6USI3C23mK20OTOcYbyQOpD9JIfBfp3nuGVUJCeflhdSs/0jYyJdax79sxvL2ANkHpUdKoPNwIiPrbbGzlba3Fu1yVo6mqIQxPDFW2UW6y5ZU+fCWI6PPD3GWN56rJv82g2w06XUug2w+81WWYj98lActQyPZDCJ42T8ecM5OuPPa7bPH7+xxEvXFjc3j45k+OTlST5+gsyjskNF0mVgxMdWW2PnqjZLTY/3nR9mqe7x+nydxYZLGEMhpXZ2rIS9xz/qTf5RMgF7VbrY6rn3y0Nx1DI8Jx1Z5lpPFIuOiTQg6mPUPIzzdM2jf/V2pe+ZMqbG37o4ztXLk1ycPBnmUV1VyVqyQ0WynoGJBlttjT03mmOp6XG30kZTFLKmzmQhxfXFJqaa3EBylv7YN/lH+fq9Kl0cRhZCdqUcLLLMleCHSedKywuPhIn0QdVOJo++sbSpefSFNebRkxCgDU0layUZDtmhIunHwIiPzW7hCoKbC3VqLRcRx5wbzWCmNfJpHUtXeWIsxwunS73A+Tg3+UfJBOyVaHicLMSj3qhlV8rBMuhlLjfodq4cvonUWWMefXUz82jO5MXnJnnxuUlmhtIHfMK9xzI0smbSNWg+wjJOyWAxMOJjs1t4xtK5udRivubgL7cpN30uTRe4cqrImWcSN3nb70wJzZmPdZN/lEzATkTDTsTB42Qh5I36eDCoZa52ZxKpGxzu+HMhBNcXOpNHb25uHv3gEyNcvXz8zaPJiIIkwyE7VCS7ZTDendj8Fh7FAkNTmComt0VNS94gRnJJAO8XdB/1NvkomYCtRENXdKw1zOqq2lccPE4W4iBv1NK38OgMUplLiMREWrcP30RaXWMend3EPHpuJMPVK1N8z6VxSsfYPKoqCmlTS6aMmrrsUJE8MgMjPjZjNJe8EczV2jhBRBSnyVr6nng89oKtREM3I7HWMOsG8Z6f8yBv1DLL8ugMQpkrigVNN6DuHK6JNIoFf32nM3n0dn/zaLZjHn3xmJtHNTURHFlTJ2PKDhXJ3jDw4mMka/L0RI60odIOIp6dzHNpKt8TJUc5jd0VR2sNszOlzJ6f8yBv1EdB8EmOHkFn/HnrkMef31+1efn1Rf7o9SUq7S3Mo1em+MhTo8fWPKqram8lfdo8nj+D5GhztKLpIWAHMTNDWS6M5fi/X1vg1lKbWCgMZwxUVaWYTl6iM8OZI5fG7mYkHD/kwmiWM8NpcimDphsA7FnJYrsb9V6WSgbVtyDpjxtENDqdK4eF40f8ecc8+tpcf/PoaM7kE8fcPNrtUMmY2rEVTZLjw8C/s3eD3Vdvr3JruU0pYzBbtYljwbmxLFGcZD8URTly6caNGQkhBK/NNXZUslgrGLKdm83aDb7AjgXFXpZKBsm3INkc2w+p2YdnIhVC8MZCo7O2fgWnzzl0VeGDTybm0feePZ7mUdmhIjksBl58dIPdnZUmKUNFQdDwQm4uN8inDZ6dLm6a/j9sc6SiKL3g3PJCKi2PMIqZGcpsW7JYKxhaXoAQkE8ZDw1g24mg2MtSySD4FiT9EUJ0xp8fnol0tZ2YR1++tsjsan/z6PnRLFcvT/I9lyYoZowDPuHj0e1QyZg6WVN2qEgOj4EXH91g98EnR7m+2GSp4XJ2OMNUMUMYi176P2tqLDfcdUJjJzf+/RYo/UTETkoWawXDK/ccEPDMZOGhAWw7ERSyVCJ5HOJY0HADGk5IGB+86IhiwVfvVHjptUW+cmd1c/PopXE+eXmKpydyRy4LuhVKd4dKJ8NxHDM0kpOHjBIdLk0V+DvvOcXX7lQQisJoVufsSIbJvMli0+fLb5dZbQdMFVMYutYrDWwXoPe7e2PtGeZqgpGsyUjO2rRk0RVDlZZHywuYq4lOyeZh0bJTQbEfpZLDzipJ9p+wYyJtHpKJ9N5qMnn0j95YYnUT8+i7The5enmKDx8z86jsUJEcdQZGfGwXzFRV5TufHGU4a/KNezV0VcENIhabPl+9vcpKy6XuhLz43CSqKnrfZ7sA3U+gjO25QRPemK8TxoIzwxnOj2b7fr9kCFKDb9yroSmgayojWZN3ny4BD3s+dioo9qNUIltuTy5eGFG3A9p+dODjzx0/4gtvrvDytQVem2v0fUzPPHp5kpnS8TGPru1QSRmqFBySI83AiI/lhsuXbpV7wfRDT44yUVz/xpLUQzVGc1ZPLDyo2gRRzMXJAl++XeHWUpMXzgz1AvJ2AbqfQNlrg+Z0Kc1i3cXUNOaqDqM5q+/3W2l6vDJb5UHVYTRvkbeSYWobX4cuh+m9kC23Jw/bTyaROn0mf+4nuzGPfvLyFN92dujYlCZkh4rkuDIw4uPeqs3bK21KaYOlRpszw5l1QbdfOUJXVU4NZZiruizUXWZKaa6cKvL8qWIvW7FdgB7NmUyXUqw0PcbyFqM5k7sV+51SSdVmttJ+5CxIVzCN5VPbBuqWF2JqWs+vkja0I+vPkD6Sk0HXRFp3AvzwYP0cq22fP+qYR++dIPOo7FCRnAQG8B1963bRMIoRIhk+dnYky0jWYDhr9sTDxck8qrrzX/hyy2e+5hJGMW/MN2h7IVlLR1XoCYW2H7LaDh45C7LTQJ2zdIayyRuspau8+0zpsfwZu/Vl7ObxsuX2eHNYJtIoFnzldoWXO5NH+w1BzVoa331xgquXJ4+FeVR2qEhOIgMjPs4MZ7gwmqXtdQdyZdZ9vpvm77apjqwpXTw7XXzk5+1+37Sp8+pcnbYfMlNKMzOUJmVoVFoelbb/WOWFnQbqsbzFC6dLj+01WbtTZrZik7N0dK3/Tpm17KbctJ8tt9LMun8clol0Z+bREp+8MsmHnjz65lHZoSI56QyM+BgvpPjI02ObBuisqdF0A16Zdchaem/w1mbsNIB1sxJ3yy0Azo1kcYOYlKFxYSxHztKpOyFzNZt2Z1bHbgPiTgN193Fdw+udcvuRgm9vp0zNZqnh8b7zI7hB9JBw2vgaNd3gSCyok2bWvccLu+vsD85E6vgRX7i5zEvXFrk23988OpazePHyBJ94bpLpAzCPCiFYbQfYfkjG1BnOGjv+3ZIdKpJBYmDEx04CdPK7Lmh6AbOVdm+IV783gZ0GsG5WopjWya3aOEGErqq90kj387OVNi03pNLyqTvhvgbExw2+vZ0yI1mWGh53yy1mhh7eKbPxeaZLqSOxoO4gzKyDkl1x/Iia4x+YiVQIwevzjd7aejd4uKRjaArf+cQoV69M8p4zB2seXW0H3FxqEscCVVV4ZiLPSG7zLbayQ0UyqAyM+NiOpM3UYDRn8dU7q1ynScONekHrUW/xvWxD3uLsSPahzEv38y0v8X0cRFbgcYNvb6dMEPHEWFLCOjuSfSibtPF5LF09EgvqDsLMehKyK5sJqMMwke7EPHphLDGPfvzSBMX04ZhHbT8kjgXj+RTLTRfbDxlhvfiQHSoSiRQfPXrlkUobgHOjuXWlhMe9xW+XeTnI7o7Hfa6NHpPRnEm55T9Uxtn4PPmUcWDtu1v9jAdhZj0JrcIb/5u/PFMgbejUneBATKRhFPPVztr6r2xhHv34xQmuXpnkqfHDN49mTB1VVVhuuqiqQsZM/rsz9STbKTtUJJIEKT46rC2PZE0bxw/RNbU3Vv36QoPVls/FqTwLdXfdLT5ragghuL3SeuQU+0F2d+z2ufrdgNfulCm3POaqDrFg3S3/MDtW+gmkjePx9zMTcRJahbsCaqKQ4tZyk7eWWgeysfVexealawv80RtLVO2g72Pec6bEi5cn+fCTo1hHKHswnDV4ZiKP7YcMZ01OD2fIWjqG7FCRSNZx/N4RH4ONQbR7Y98YVNeWR4QQvPqgTqXl8aDqADCcM8mnjF4w3W3XRz8OcqHabp+rXwkB3lk8V255GKrKpenCulv+fvxMO/VSbHzu5YZ7oGWQk9AqbGoqLS9k6UENVVX2tURg+yFfuLnC515b5I2F/ubR8bzFi89N8onLE0xtMhjvMEk6VHTGCimyskNFItmSgRIf/Uon8zWXKBaoCswMpbF0FS+MsXQVIQSzlTZzNZuzwxkEgomixaWpwrrFctt1fewFh2lg7FdCgHcWz9VsHz+KDt1IutufYT+F3nHezuv4SeeKF0acGc6s69zYS4QQXJtLzKNfePPomUd3gtptibV0MoaGesTOJ5EcVQZKfGwMQCtNr/f36/MNlpsemgpvLrUYzhpkLZ04ElTsgKWGxxNjWS5NFR7qmtiu62Mv2E8DoxCC5YbbM/KdGc4wXkj1xM1mJYTux0ZyJtOlZG7JYRpJt+IklEH2EyEEbT9KhGTHRKooCiM58yHD5ONSaXn80RtLvHRtsZdN3MiFsSyfvDzJdx+ieXQzdFVNWmItjbQhW2IlkkdhoN6BNwagsbzFfM1lvubgRxGGpiIQzNUc/CBpIZwppfnAk2PMlpOR7GsD6067PvaC/by5LzdcPndtgTcXW1i6xnPTBb7rmbFedqfh+KQMlSCMMHSVhuOTTxlcmSmsW0Z3EG/C6/8NwQ2iHXltTkIZZD+IY0HTDWm4AUG0fybSrnn0c68t8tU7x8M8uhZDU8mYGllLlx0qEskeMFDio58JcTRn0fJCTg8nQf3GYoMgjKm6AbYXsdL0Waq7zAwlwmLtG2K/gPaob5jblVX2+ua+9vluLTV5c7GJHwrCOO4ZM4F1fpdiyqDuBpwaSjOSS372C2O5Xf8sj8Pa19wNor5G134c5zLIfhBGMQ037LWM7xezlTYvXVvkj7cwj777TIlPXk4mjx4l86ipq8nAL0vD0o/OuSSSk8DAiI/NAmL3BixEklUoWBqOH1Nuujw5miNn6kwWUz2fx1r2MqBtV1bZ65v72ue7XW4RxgJFhaYboqqJ2OlmW4oZgzvlNoYGQRRTzBhEsdg0+7KfJaK1r/ntlRaxgKliihsLTa53jIondaDXXuCHyfjzlhfu2yTS42weTRlaT3DIDhWJZP8YGPGxWUBc+3FVgelSihdOF3l7WWM4YzGcM9f5PLo8yu2+39d0z3Z9oUGl5XFpusBCzX0osO/1zX1tGadqe5wfgVgINE3lI0+N9s6mqQqVlo+hqQRRkn6u2wEjOWvT7Mt+mzvXbiBuuj73Km1mV9uc9XIEUczzp0rHbqDXfuMGETU7Gfu9H3TNo5+7tsCf31zB7TN8zNAUPvTkKC9ePjrmUUVRSBtab8roUTiTRDIIDIz46BcQx7rdLFWbc6M5FmsOy02PkZzJeCHFmeEMZ4YzfWd4PMrtfquW1dWW3zPfbRXY+/EoQmhtGWcka3JqKEMUi97m3m5W6PlTRZpuwJVTRUxNwY8Elq6uazXe6nvvh7lz7QbihhOy1HRQUFAQVDqt0/tZXjlOo9O7k0i9YH/Gn1daHn/4+hL/92sLLNTdvo95YizL1ctTfPzSOIUjYB6VHSonm+P0+znIDIz46BcQV5oe91ZtlpoeS02PvKUxlDVJ6Sq3lh10NXmTmq+5D/kKHuV2v1XL6sWpPMC6Vt6dsp0Q2mxI2FrvxHzNIYwF1xcatL2wZ5wdL6R2nUXYb3Pn2g3ESw2XUsYkbST/nmlD3/dOlqM+Oj2OBU0vpOHsj4k0jGK+fHuVl64t8Nd3VvuaR3OWzndfGueTlyd5aiK/52fYLVpn2qjsUDn5HPXfT0nCwIiPfgHxTrlN1tL5jnNDXJuvkzU1HD/i8zeXWW76rDQ95usOI5kUF6fzXJ9v9HwFWVNDVeD6fAM/ijg9nEYIseWb2lYtqwt1d9MSz3ZsJ4Q2+2Vc652IYkgbGq8+qNNyw8dabrff5s61r2PWSgJKHCtYusq7z5T2vZPlqI5Oj2LRWWe/PybSu5U2L722yJ9c728eVYDzo1k+8dwEP/SumUMfIy47VAaTo/r7KVnPwIiPfgExZ+noqspSw8MLBFZOZ7XtoSnwZGfdfRBH+FHEG3N13lxqUW56rDQ9PvTkCDNDaZabHoamMl9zGM1tPbJ7s4zA42YJsqZG0w14ZbYTjM31b7Tb/TJut9fmqLH2dez+rAfZ8nvUZobsp4m07SXm0ZeuLfDGQrPvY0ZzJldmirzrdImRnMUzE/lDEx6yQ0Vy1H4/Jf0Z6H+VbhC7vtBAQeHiVJ4bC00EsNTwKLc8nhrP8u4zJW4tt3qll2tzDQxNYaqYQlOhmDGotHyabtATH5vVHftlBPYiS6AogNL53w1s98u42V6bo/pLe9hts/1E5GHUmd0gmUTa9vbWRCqE4LW5Oi9dW9zWPPrJK1O863SRmh3u2yTU7ZAdKpK1yJk+x4OjGV0OiG4QAwiiOgt1l6GswVTJQlGSMkUhbTCas7D9iJtLLWw/YrnpcH81ERwPak6nEyRmKGP0jJgHWXdMbv0GT08ku1Xa/npz4Xa/jN3XYeNem8P4pX3UIH6Qwb+f+DnI3TFtL6S2DybScsvjj15PJo/O1fpPHn1yPMfVy5N898X15tH9mIS6GbJDRbIVh305keyMgRUf3WDVdAPcIKKQSkxoZ4YzNN2A+ZpLMWNQt5N09pnhDE+MZblbbjOWT3H5VIm7K8kY9tGcxfWFJvM1B1V9Z9vt2lJH0w0QQmw6wvxxfg43iFhputTtgKGs8VDGYqtfxn5B+zDNWY8q2g7bZHYQ7cUNd+9NpEEU85VtzKP5lM53Xxzn6iGaR2WHikRystgX8TE3N8fP/dzP8dJLL2HbNk8++SS/93u/x3vf+979eLpHohusutM7Tw9lGM6ZKErSTvqg6nQGa6lcOVXkwliKDz81xpnhDLMVG9ePyKUM8mkj8R5YOufH8j2vxMZShxfGfONemdvlxFfxxFiWDz819kgBcq1gcIOIuZqdZF/imJmh9CN0ytSotHzCWPDuMyUuTRV6n9ssk/Coc0622iEDjx7ED9tktl915igWNJyAxh6bSLvm0T9+Y4ma0988+m1nh7h6eZLvfHL0UDwcskNFIjm57Ln4qFarfOd3ficf+9jHeOmllxgbG+Ott95iaGhor5/qsegGq0JapzbnM5o1WG1B0w2wdJXTQxkKaZ2GE2Lpai97MJozyXbadE8PpxnJmtyvOg95JTaWOppuUpsvpU0gmQ76qAFy7S1/peliaCrPTheZrzmkdvAmvVY4VFoe5aZHy49Yabg0HJ92R0zN1xyiOAkCV2YKKIrS+3kSX0Bj13NOvnSrzNsriQC7MJrlI0+vF2CPGsQP22S213Xm/TCRtr2Qz3fMo9c3MY9OFlK8eHmC731ukslDyIDJDhWJZDDY83fo3/iN3+D06dP83u/9Xu9j58+f3+uneWy6wWp2xWah7rDUdMlbOlMli6cn8gznTKJYMJwzyafeqW2XWz7zNZcoFizUPcbyKd57bnidV2I0Z/adZJq1dJaancxHLrsuQO4mk7D2ll+3A4I43lXQXSteWl7Aqu2zUHNRFbizEpAxNTRVXSdq7q3a1J2wJzaKaf2R5py0vJBS2gAU2n0E2KMG8cM2me1VnXmvTaRCCF6dq/PyNubRjzw1xtXLk7zrTAn1gDMMskNFIhk89lx8/O///b/5xCc+wd/7e3+PP//zP2dmZoaf/Mmf5B/9o3/U9/Ge5+F5Xu/vjUb/XRB7TS9YuT5pU8ULBJW2z6sPajw9kd80kLW8kDCKSZs6d8stiul3fBLdwNPPfDiWt/jwU6OcHckA9DbkdkXHbKXNvVWbbKf9d6tMwtpb/lDWYGZod+vs14qXuZpgNGcxX3Oo2QF+LChlLTw/XidqgHViA9h1piFnJQPAlhrvZD5240/ZisMyme2V0XWnJlIhBKvtYF1nSb/nW2l6/PEbW5tHn57I8eJzk3z3pfF1AvsgkB0qEslgs+fi4/bt2/z2b/82n/70p/kX/+Jf8LWvfY2f+qmfwjRNPvWpTz30+M9+9rP8yq/8yl4f4yE2M1bODGXIpwwMLdnt0nRDbiw2uTRV4Pxo9qE39u7CtVfn6snfV23OjmTXCYXNBMpEMc3EhiVaXaHyYLXNnYrNpak8Kuq6tt2NjOZMpkvJXpq149BXmh53yu2H9sZsDIxrxYuuqpwbydDN7L+x0KDW9pguZdaJGiEEdafRExtnhjPryjA7ET1jeYsPPTnKmeH1Auw48zhGVyGSSaR1e+cm0tV2wM2lJnEsUFWFZybyjOSSLpMgivny7QovvbbI1+72N48WUjofvzTB1cuTPDH+8Ebi/WJth0rG0NCl4JBIBpo9Fx9xHPPe976XX//1Xwfg3e9+N9euXeN3fud3+oqPz3zmM3z605/u/b3RaHD69Om9PtamQeLMcIYnx3K8+qCOHURoCizWXIJI9A0kSTtqhrYfcm4ki9NnGNdOBEqXbhZiKGvx1btVvDBiLJfiuZk8S3WnrzlzbelnvuYymksC+GZ7Yzb+zBvFy3DGoOFGhFHMlZkiZ0cyvfHqXfElhOB5RaHpBnhhTMsLyaeMvgJtMxRF6SvAHoWdZhz2uwX3UYyuj2Mitf2QOBaM51MsN11sP6RRDnj52vbm0U9emeSDTxyceVR2qEgkks3Yc/ExNTXFs88+u+5jly5d4n/9r//V9/GWZWFZ+3/73SxIjBdSvO/CCF4UU254hDGMFyxWmj5vzNcptzxMLelWaXth5wankjU17laSLMPGiaI7EShdulmIWttjPJ+MV1c6fojrC82+3TFb7YiZLqWZq9rMVtrYfsRqy+fiVJ6FukvTTQLTbKXNbMUmZ+nM11xGsua6MtNozqTc8tdlUdbORLnzGDf9vRICO8047HcL7m6MrkEUd8afP7qJNGPqqKrCvdU2ry80+M9fmeXWcqvvY7vm0U88N8nEAZlHZYeKRCLZCXsuPr7zO7+TmzdvrvvYm2++ydmzZ/f6qXbFVkHC9iPSusbZ0SxvLDT46u0KXhhzu6LhBzFThRQLDZeImKxpMJIxUFUFVVHoF0O680JmKzZ3O/tjugJlYwAezZm96aK5tNHzfCiKsml3zFY7YrozRRbqDm0vZLWdzBcZyVt4YcydBzVuLCblk/edHwElGVJ2YSy3pWelG7Afp6V1s7beRwlQ252j+zpfX2isE2B73YK7E6OrG0Q0Op0rj4MQgvurbf74jSW+/HYFv0+p5jDMo7qqkrVkh4pEItk5ey4+/tk/+2d88IMf5Nd//df5kR/5Ef76r/+a3/3d3+V3f/d39/qpdsVmQaK72fZOxWa50/HS8AKCSGDoKvN1h+GMTtsLMHSVKBLMVR3OjGZ4z9nhvhNFu/Qbeb52HXzLC3sljm87O7SuY0YIwWzF7tsds9l4724ppe7AStOlmLGIhE/KTAysTTdIAn8kqLQDvnJ7lfeeG3rott50A1ZbPoW0zmoroOH4AL25IqrCI7W0Jq29Pk0vpNz0EEJsuw9nM7bLOGyc4wIwnDP3vAV3K6Or7YfU7GSI3eOw0vT4w9cXefn1ReZr/dfWPzWe45NXJvlbFw/GPGpoam+pn+xQkUgku2XPxce3f/u38/u///t85jOf4V/+y3/J+fPn+a3f+i1+7Md+bK+faldsFiS6A8IuTeXxwoh3nS5RbnrM1VwsPdlc2/ZiFEVhvu5gaD5DaQMhkgCsKsnN9vZKa10pYbOR590be9rUeXWuTttfv0G2ez4hRN/umM1+lpWm1/OBlJse7SCiRNLeO11KM15IJZ0SnbbaM0MZCimtr+nTC2PuV22CcoyhqUwPpbhbcTqZEHbdXdMlZ+mEnfON5S1MTXvkTMR2GYfu63xpOhmYNlG0uDRV6D1uv7wgj2Ii7UcQxXz57Qqfu7bI14+IedQyNHKmTtrUDn1jrUQiOd7syySm7//+7+f7v//79+Nb7zk5S0dTFJpOiB9GvDHXIJdSmSxYFFI6xlSBU8UUq22DtKEwVcqQT+k8MZZjNJ/CDaJ1w7i6ImKzm3lvg2w5qdOfG8niBvFDQXi35sy1ZYia7aOoYBkqT+Syve4SgJShoqoKQSSYLCZZF0VR1gXjlhswM5SilDGp2wFhFK8rcaQMjQtjuw92Y3mLd58pIYTA1LS+o+B3ynattd3XeaHmMpJLhMfaDMteloAg8ds03YCGExLGjy467pTbfO61Bf7k+jL1Tcyj7z2XTB7db/OooiikjCTDITtUJBLJXjIwu1363XS7H49FzP3VFvcqNg3X5/RQliszBTRNJWcm49bn6x6xolBzQoazFudGc4wXUtxeaRHFPOQ92OxmvnaDbG7VxgkidPXxN8iuFTvDWZMrp4qdWQpJSvz2SotKy2OykOLCaI67lTbnRjOM5kyWGy53yy1en2+gKhDFUEjrKCiMdMoi8zWXuZpNuzMV9VGyBYqicGmqwGjO2vdhYDvJjOxFCahrIm25IfEjmkhbXsjnbyzzuWuL3FzsP3l0qpjixecm+d7nJvbVPKoqCmlTS6aMdsytEolEstcMjPjY2PVwZaZApe3zjXs17lfaXJtvstz0iEXMg6qDqas8KRQiAWesDLqmMJXL0HADCunEKAqbew82u5k/ygbZnZQINgbbbsfK2uFlrc7NXFMVspbOmeEM5ZbPqw/q3Fio8/pCkyfHskSx4NRQmicncmRNDSEEbS+kavsIAZWWv65UtBsOahjYTjIjj1MC2omJdKuBYEIIXn1Q53PXFvmLN1fw+kweNXWVjzw1youXJ3nX6f0zj2pqIjiypk7GlB0qEolk/xkY8bGxO+Leqs3NxSYPqg52EOH4EQpgKEkQqtsBY4UUi3WHctMljAUPajZZy6DhhJRbfk9E7Gas90YhsZM5GTtpF90YbLsdK3NVm6Wmx/vOD1OzfbwwImPpPRNs93UZzaeI5xv4UYymqgxlTS6M5VhuuL0dLkt1h1U7oJQxCWLBuZH0I7et7vf8je141BJQ2wupOzszkfYbCBYLsa159JmJPC921tbnUvvzK6qram8lfdqUhlGJRHKwDIz4yJoaTTfglVmHrKUzlNExNY2xvMWdlYCpksVSTWAHMTrJTXB+1Wa8aDFTTLPYcDtGzTRRJGg4PkKIdUPAdhJAdzp3Ym1wLjddyi2XUsZMSgVbTD/t0hUV50ZzLDU97lba6KrKSDbFpel3TLBJ5gYajk/O1FEVhQujmZ5PZK1oe2OuzqtzNQxNw9QVLk3meXKi/5nXZl/6CYz9nr+xHTspAXV/noYbJMJUUwl3MRSsOxBsOGPyxVsr/M+v3efafH1z8+izHfPoI/hpdkK3QyVjarIlViKRHCoDIz6SLoSkhTRGkDFzDGWTlsSnJ3I8M5njb2ar3Fu1iYWglE5S5NPFFE0vZLHu8dZyi5VWsgsmk9Jw/Zg7lYeHgG183rUBudmZarndnIy1wXm+ZnN/NSkFGZrKlc700n4/Y/e53CBCU8HxQy6MZjk7kiFr6cxVnXUlorG8xXQpzULN4dJkActQeHb6HSGwtqwEMcPZZPHeg6pNuKGbY6OgmC6leh04ezkvZK+yJtuVZhbqDl+9vUrbi0Bh3SjznbDS9Hj59UW+eb+G3acd+x3z6BQffGJkX8yj3R0qskNFIpEcJQZGfNyvOqw0fUppg5Wmj+1HvHC6RMsLcfywV3c3NJWWG3BrpY0XxYzkTVbbPlEcUXd8IAZS3FpsYOr6Q0PAxoRgueH2MiIZU2O+5hILegF5JxMx1wbnhbrNcNbgyfE8DSfE6hNEhBBcX2jwymwVU9MoZXRODWceaondeNNPOho0xgvpdd0s3WC+tqyUtTTi2xWqtk8pYz4ktDYKipWmt6nA6OeV2amo2C5r8rjipLvO/tZyi6YbrhtlPsJ68bHR12FqCp9/c4WXtjGPXr08yfc+O7Hn2Z61O1Sypt5bCiiRSCRHiYERH0IIbC8iikTP3NcNyK89qHG73MYLImYrbYSAfNqg4QS8vdRE11WCUFBt+wQRjORAoKKqUOsM4OoOAVtpenzpVpm3V5KMSD6lM5KxeqUOS1cfMoYuN9wtl7/lUwY5K8nEDOfMvkOkVpoe37hX40HV6f1cT0680xK7VUDeamDX2uzAuZEMw1lz3UK7tWz8PmN5i/ma2/f79vPK7LQUs13W5FFLOo6frLO3/cREmjaSbo/lpovaGRu+kdV2wPXFBrdX2nzjXpU3FhoE0cN1la559JNXprgyU6Bmh9h+0nGz2WbanSJ3qEgkkuPGwIiPtKFSbXtU2x5DWYu0ofaC1P2qTauz90TXVHRVwQsjFBSCWGApClXXp5Q1KaQMohgsI1kJrygKpYzRW8R2p9ym5YWU0gag4AUBlbbLK7PJMLOcpfc1hm4MlOsyDh1DYNej0c/U2vJCdFVhtBPELX19++76gJy0BnezIt0R79uZZlVV5dnp/iUf6N9xs5mnol/JY6elmO2mm+62pNPqmEg3rrMfzho8M5Ff162yluWGy//8+gM+f3OZqv3wTA5IynEfenKUjz0zzunhNIqiUGn5m26m3Slyh4pEIjnODIz4mK+7NL2QlGnQ9ELm6y7ZlEkUCy5PF7mx0KDS8jp7WFRW2xGWnqSwz49kGc1bPDmRp9zySBkqacNAoKCpam/mBySBMWfpLDXaCCFI6WoS3NyAQkqn3PJ622mTEept5qptSlmLWtujmF6/yG3txNNu5gJ4qJSQs3RGciYCgR9qmJrK3XILIcRDy+jemK+zWHcZy6d6bcc7DV47MZWuzTJs/Bn6ZXnW/gz9RMVm+3A2E0s7WfYWx4KmG9JwN59EqigKIzlzXanFD2P+6u0KL19b4Gt3q/Szn3bNox+8MIIbxsSx4EEtMTqP5My+m2k3lnP6YWhqMn9D7lCRSCTHnIERH44fEccCXYeaHbJYd3jX6SFUBd5cauKHMaWMiaGrtNyQrCUQioKhqVwYz+GFMZWWz1g+xVjOJBYwM5R56GY9lrf4zidGyKd0Fusuiw2Hhh1S9wIWUFBQGM1ZTBTTrDQ9Zis2t8s2K3dWGc+nyVpJFmVjmWC7UkKSdSgl22y9iLuVNndXbZ4Yc/jwU2PrAnIYJ+2la9uO6064ozLFbkyl231t/5+h//6dfl+3WTZjNGf29tyM5a3eTBaAcM1m2d0MBXt7pcVL1xb5kzeWaLgPz/ZQgBdOl/jBF6Z6k0fvr9rMVuyHREZ3M+1W5ZwuskNFIpGcRAZGfIzkLCIhuFNuo+sqNTsJIDNDaV6fr1NImahpaPkRKTNmJJflzEiW4ZzFVDFFIW2uW/r22lxjU4+EqqooKDScgPmqQyjg3qrNVD7FYiNZZDdRTPe+36WpPLW2x0hGY7Xl8Ve3VpgZStpdu1mSbuZiqpTi+nyD6wsN4OEMiO0nM0uKaRNFoWeEPT+a7QX208Np5qoOc1W70xkT4gYxl6YLLNS23vy6G1Ppdl/bb6T8Zvt3ul83V7OZrbS3NJOWW35PEM3XXEZzFsWMQd0JaHvRjtfZt9yQP72xzEvXFnhzqf/a+q3Mo5uJjO3KOVZnMm3G1GWHikQiOZEMjPiYKqa4PF1kte2Ts3SKaYO2H5EyNKaKaXKWzmzFJhIxGSNNxlRpuiEzpTSFdNLZ0e1kma20iUXM0BqvR5duKeXmUoO6G+AEMbYfkdZ1Tg1lMI13gknO0tE1laYXEQm4u+pSd3yGcyajuTYXRrN85Omxdbtirs83eFB1Ej9KVO9lAdZuca20fWIBGUvrGWHXBvbuKPHZSpu2nwiP7ubXkZy1ZefJ2gyKqiTeg5WmS90Oth3UtZNyyHZf1/ZCWm7IajvYNNOyVqzcKbe4U27veIx7LATfvF/j5WuL/MVbZfw+k0ctXeXDOzCPbiYyNpZzujtUMqZO1pQ7VCQSyclnYMRHPmUwXrSoOQGRgKz1TqDseiXyKY2a47NUbxEJheGMzvsuDPfS9itNjy++VeZ2+Z3ZHudGc+tu3t1SylzVZbXtc2Y4g6YqqIrCeN7qpdBvr7TImhpXZgroKhALDE3hlftVhtIGpbSZBNoNu2KuLzRQUHhmKseNhWYvA9KdH3JpuoAQgrSZlFX6ba3tCpGWlwTxqWIKBWXd5tfNSh1rSyNuEDFXszE0lSCOmRlKM5a3NhUuu50G22Xt11VaHpWWv2WmJWtqeGHEqw9qCGCquL2fZanh8kevL/Hy64ss1DeZPDqZ55OXJ/nYxfHefztbmUf7eUbW/htkOjtUMrIlViKRDBgDIz6EEIgY0kYyvfTSVK4X/LpeifurDqstr2cSDCKdt5ea3BjLcWmqQMsLaXvhQ7M9NnZs5Cyd918Y4Su3y+iqynQpxfmxHFPFFF4YP7QF99JUgXLLZ7HmoAiFxYaDGwouTxce2hUjhKDc8vjiWyustpIOiyASTBUtmm7A4qxNLODCWLaXldnMTNqd+rpYT8yQFyfz2w4BW5tBub3SIo6ToWRr54Ns1sHzqHtd1n5dztKpO2Hf7EkUCxpOgBNETBXTFNMhGUMDIbi/aj+0XyUxj5b53GuL/M1sf/NoMW3w8UvjXL082XeT727Mo3KHyu457DH8EolkfxgY8XG/6lBu+0wWs9QcHyeIex0n0N3Z4REKQdsN8EJBxtS5X3X5m7urjHbKEVlLZ6nZyXx0Shpr6ZZSAJ4az+MGIV4guLPSYjhrYunqQ1twM4aaTF9t+6AIJgspdE2jkE7KH0KIdW+4QoAXxERxjKWrzFVtojgCkg6OB1WHlhtwb9Xhw0+NMlFMb/q6KAqgwMb38+7Y9Tfm64Sx4PRwuneObkCotDxaXsBcTazbzPs400u3o1/2pDsUrOWFvX/PbsahX2aiZvuJefR6f/OoqsC3nxvm6uVJPvDECMYWZZDtzKNrd6ikDFUGzl1y2GP4JRLJ/jAw4iPZzBoQRTFu+I7psPvmdqfS5m7ZZrHu0e4EpCCK8MOIuZrDbKXNt50d4sNPjXJ2JNl7cmb4nZX0TTfAC2NMLekAsXSVkZzJ7ZXEHDlXc1Hv1XjX6SItL+CVe04iZkyNe6s2K00fQ1dxQ8FoPk3VDlise7ymNni+c/OHzqyPlM6TE3k+f32JL9xcZqqUxvZDhrMWo3mL1+YbFNMmt8ttznRmS3TPZ+kq+ZTBWN7qzA0xeHrinV0vXcbyidH2zcUmsRC8PldnJGv2unRefVAnjGKEgJGsyZnhDEIIbq+0eqPdd+vt2AlrsyCOH7HU8HpDwbr/zmsnjrb9gDgW5CydP7uxzP/7i7e5W7H7fu/pUtc8OrlpSWjj9x/q4+uQHSp7x34KWYlEcngMjPjImBp+GLPS9CimDTKdwV3dN7dTpRS6qmBoCoWUTiBiCqlkJkjNCbi3anN2JMtEMb0uk9AtMVRaHg+qDqeHMgx35lDkUwY3F5usND1G8xa6qtD2QoQARJLBWIula6iKwmLNwTJ1zo3mcIPoobHkbS/k7ZUWipp8n4uTedwgJowFVdtDVRRMTaHhhdxYbHK/6hBEMXNVd935tptsavsRLT+ilDa5U7E5t6ZLJ4pFr9V4JJekwrs3VFVJuog2jnbfC4QQvaFg/cygazfJokCt7fMnN5Z5Y77Rdymcpat819NjXL08yfOnittmJvptqh3JmUwbadmhsg88qklZIpEcbQbmN7nlBjS9EBELml7Ym2iaNTVaXsBC3SEmCUalYorFmkvNDihkDC5O5PDDiL+6tdJbP15KG+RSRq/8UEjrBOWYQlonikWvvfU9Z4dQFAVdTcyHiqKQTxk8M/lOtuH0UJrRrMlqy+PiRI6LkzmaXkzb9VlseDh+wHzNYbJgkU8ZnB5K03JDnpnIc2OxSc0OmC6lmRlK03IDsqZOywvQAoW6k3yPM8Npgujh8+3MALo+aPcLCBtvqClD6+uReFTiWNBwAxpOSBj3HwoGiQdjtZUsAfzirTK1TSaPXprKc/XyJB99ZnxXAW2tx6Nm+1i6ypnhzEMdKtKrsDc8qklZIpEcbQZGfNxbdZit2Kgkq+HurTq8H4jjmLmqzd2VFqaqMJo1cIKIIIywFUFkw5feXsHQkoDbcpP9LnnLoJQxuDCapelFFFwdP4q5tdJkqpjcgvutbRdCUHfWzwgRQlDMGGha8vfnT5WoOiFfv7vKW8tNNFWhZge8cKrEUNakkE68J6au8uR4jjPDmZ65VAhBLmXw6oMaKT3kyYkcX7tbpdzyMDSVhhMylDVwg4g75TZZU+sIsIcnp54ZzvDEWDYRKpkMbhDx5zeXGc2ZXJ7OYwfxuoCw2Q11u0C81eeDzlCw1jZDwfww5i9vlfnf35rn1Qf1Tc2j3/vsBC9enuT8aHbX/w0pSjIgruEk3pLRvMVkMdW3NVZ6FfaGRzUpSySSo83AiA8niDotqDotP8Tp7PF4bb7BNx/Uk+FcXsBozkQJBYamMdYpJ9TbIflUkr1o2BF2EOIFEau2x3QpRSGlM5Y10BQFVVlfTum+eY6tCbBdT0jXe3Gn3F7nvbhfdbi36vDmQpOFusvl6SLLDR8vjLhdbjORN8mlDEZz1kMdLStNj/mamww5c0OW6x4XRrOcGU6TSxlYuoobRLwxX6ftR8RCkLcM8injoSA5Xkjx4afGaHlJd8lX3q4QxgJDU7l6ZXLdnpetbqjbBeJ+ny90Fvt1RdFmvL3c4nPXFvnTLcyj33F+mBcvT/KBC+vNo13/RtsP8EOBqStkTWNdR4yqKD3DaLJDJflZt7uJS6+CRCKRbM7AiI/JYorxgoWuqGRSGhMFi+WGy51yi2orMXsKoGoHxICiwv2aw1Da5OxoholCijfm66y0HaJYIQgFigJvLTU5N5rn7EiGtGUmUzirNvdW7aSNteERhIl3otb2MXWNkZzJ86dKvdZZN4gotzxqts9IzqRmB9wut7EMjYYb8tZSC1NXaHsRiqp0vCAxIzlr0wFbl6YLvZ+7O7ujG1C/dqfC7bJNKW1wp9xmppTqlYHWBsm1t877qzZhLHhmssDNxQYrTW/d8251Q90uEO92KFjTDfjT68u8dG2Rt5b7Tx6dKaUT8+hzE4zm+n+vrn+j3g5YaDhMFlKUsibPTRU4NZwhZ/XvUNnJTVx6FSQSiWRzBuYd8fmZIjeXmizXPcaLFjOlNK8+qNN2IrwwouEFKEDe0kjrOmpKAQHPzeSZLKQTE+FUgVLGpG4H1J1EsFw+VURFxfZDIgFztcRP0fZDvvkg4Fv3qqR0jXLLYyRn9YaAdUeEu0HEg1UbQ1XxwpCUoVFuurS9kMm8yYXRDNOFFKdHsuRTGi0vwgmida2ta+kGvYWamzzfVGGLdL+CpWtomrppkOyWRLwwwg9jbizUMXVtV7X37QJx1tRwg4i/fKuM23kNRjv+mG52ouUF3F5p85e3ynzxVrnv2vqUrvJdz4zx4uVJnp/Z3jza9W/kUhpxTTBRtEjpOsWM8djeAulVkEgkks0ZGPGhKAo50yDICHKmge2FVJoeupbU8aeKFmEElbZDuR0RRRFnRrOcHc5RSJt4YcgLZ4bILDSYr3s8MZ7FCWL8IMaPYgrppGwxkjUZzhjcLrep2z5NN2RqPMVKy8PUYbbcJmWoOGEyCKvS8jFUlUvTBa7PN5it2Ghq4p+IgEuTedp+xL1Vm6GMwbefG8INRUcUJC2+3fLNZlNEN3oqTg+luTCape2FPDdd4NnpPGlT7xsk15ZETo+kGc6YPDWR5+Jkfsev/WaBeO1QsLSp4UWJqFpuugxlTEZyJjcXW/x/XnnAK7NVas7W5tGPPTNOdhcZhlLGoNpO/lsYyVnEMaRNbU+yFNKrIJFIJJszMOKjO2SslDYpt31mV11uLjW5vdKkavuoSjLn4VQpy6khBZRkA2rN8XlqMs9iXVC3A/Jpk7QTcno4w1AmmengBYKLU3kW6km2wQ0i5qouC41k4uV83SVn6WQsA8+P0BWVlYbHhdEcmgrllssrs8n01OGc2fNSpA2NB6ttvnm/RiljcWulnWQRNJWFukOlGTBdSpE2Nd5zdohLU4W+QW/jxNHL03menS70tr5enMyjqv27Na4vNKi0PC5NF1AVlacmcrvuYtl4pmQomL9uKJilqwxlTMbzKeZqNn92Y4m/vlvllU0mj5bSBt/zCObRlNGZMGpp6KrCVDHddwbKXiI7XyQSiWQ9AyM+ukPGwjCi6gQULRVDVxjOmlTaPvcqNnYQUUobZCwNU9dQUWh6EV+9s0re0hjOWjx/oUQhZZAyVaaKKdwg4rW5Bss3bdKmTimtUXMCLB3ee6bETDHNZMHkzEiOIIxYbvhYhspX71T40zeWMHUFVVVImyppMwmICzU32ZcSCSptn6odMpZP0fYFD1ZtQhSiOOZOpU0kIjKm0evE6JZY1ga8SssjjGNmSpmeobXuhOu2vm4szSw3XL50q8xC3WG1HSAQjOZSj5UVcPyIuhP0HQpWs31ul1v8n9fmee1Bo2cIXosCvPtMiR961wzvvzC85eTRd763T6Xtk9JVnp7IJ3ts1gT+8UJq37tQZOeLRCKRrGdgxEfG1HD9iDdWarT9CF2BSAhuLjWYr/soIiZWoGH7PDNZQFMS/8ez0yVqdrLITFUVFuoumqawavvcr9o8WE12qQSdaZ/LLZe6HWJqKiutgMmCxbvODHNxMpnJ8cZ8gzsVm5WmQ7UdkEvpWLrGs9NFLE1FUxUsQ0UhmcdxZabEzaUWCzUnmeUxnGZ21SGMkjHwi3X49vNZdFVZZ+RcG/CaboCivDNxFNi2E+Peqs3bK22KqeQcaUPj+VPFXWcFthsKNlux+f9+Y46/uVtlodF/odt43uK7nh7j45cmeHI8u23WQO0sbWt7IXcrbe6Uk4mm5Zbf2xJ8kAxa54vM9Egkku0YGPHR9qLOLThps52v2YxkU2iKgq4I3BCqbZ9CKpmBEQsFiHhzucWF0SwvnC6hKEmAv7Xc5O3lFi034K2lFufHcmRNnTuVpEOlbvs8PVGg3HKJhGCuZrPa9rlbbrPU8FhquChK0lmTNlQW2j5/cWORy6eGURRQVYVYJN6UKI45PZQhY6qcHs7y7FQB2495c6nJZCmNoSbG2JGcuS4rsTbgzVVFsuuks5+m36yRzVAUlYypkTbemQUymjMpt/wtg0scJ3tm6k7w0FCwWAhema3y0rVFvrSJeVRVEtHx3HSBH3h+iiunSpsGsOTnCYgFjOUsTg+nUVWV2ystbD+imDJoeSGz5TazI5kdnX8vGbTOF5npkUgk23Gy3wXXUHN8lpsufhSjqVCzI8YKCpPFNJBsYo2EIG2oLDZcdFXlfU+MoCGSOR55C1VVGQcqLQ8niKm6AV4Uc3ulRdbSQEkyLE1Xoe0FlLIWV2ZKLNZdFmp13FCAmrwhu36M3dlNIhSFCJVK26PphhTTJuWWx3vOlJgspni3MsTFqTw3F1pU2gEzpTSqAmdHcyzUkm2txbRBHMcs1ZOpqW4QoXayHbqm9uaBdG+kU0WLthf2Oko2Lq87M5zpmVLHchaNjtDS1GR3zXzN7Rtcws5QsGafoWCLDZeXry3y8rVFlje06nYZy5lkzaSbZtX2CaOYhbrHzFDQW1XfLdV4YcRoziJtqKy2A6JYULMD0qbGeCEpEeUsndsrNZabPuN5i3urNllL3/T8+8Ggdb4MWqZHIpHsnoERH6zZyBrFUMzofOTpMVYaHq/N1RjOWoRRCKj4UYwbxrz2oMaF8RxNL6Tc8nsB6sxwhomCieMHPH+qQLnpM5QxcPwYRMzFqQJPj+do+0lbbBgLcpZJLg1vzLt4QUjT8UkbKpoqyKYM3nduiFUnGaNuBxEtN6Tc8nhupkgYw82FFverNoIkYOZSBq4f4QYx1baDFwjultuoqkLOMtDUh/errL2RtrwAIZJb+WzF5uxIZt3AsvFCio88PdbzjFTaPlPFFDcWmpRbLipqz2Tb8kKKYeLnaHsRcRz3lq/pqsobC3Vefn1pU/NoMW3wbWdKTBYsFuout1ZazNeS9uMnJ/JkTK23ql5XVRw/ZKXpJSIucCh2RsZvDHZjeYsPPTmKrircr9pcni7idvb7HGRwHLTOl0HL9Egkkt0zMO8KsegEbUsjFvChJ0f4vitTlFs+z58uIYRgse7w/3t1gWYjROusmb84nieMBNcXGkAS0MYLKb7rmXEK6RpV26OUtnh6Is837tfIpXSmi2ne1SnT3Fu1URWoqQFV28P1Q2IBQlEopA0MLVnD3vIjJgspHD+m0vK4OJlnOGNh6SrPnypyfaFBLGImiilmV1pMldKUMgZ3yi3maw6LDYe6EzCSNfnI0+O4YUzK0Dg/mmWl6XGn3E6Mp1HMzFCGV+45IGAsn+LVuTptL1met3ZUezdg5iyduhNyY6HJ/apNIWPQsBN/RjaVCIO5qtN7rVfbAZ+/uczfzFZ59UG9r3lUVeB950c6k0eHWai7fP3uKmEskmm0gKkppHUVy9CYKqaYLiVi6vZKNwOTiAfoP9pdURQmimk+8MQo2Qd1vFCgqwqaqrDSdKnbAUNZ48gHx+PmoRi0TI9EItk9R/tddw+pO0HiP4hidE1N5nJoWi+bcW/VpmoHCEBRBKt2iO2HvDZfZzibpPuDSPRS9N05F28tNam0fGbLTRpOwKmhNFEsaPsR+ZTR6yppB2GSFUCQNpOOGBXB2aEspazFeN7i/RdGuDTl8417NUxNo5TR8cIYxQsZy1ss1V2+cGMFL4yIEAjSyQj1lseDVZsgFoznLSLg+ZkSOUvvm+2YrzlkTY2mG/L1uxXafkAhneUb92q8PldjLJ9kPZ6dLna6aEymSynKLZdCxuDbz5Z49X4dXYPxnIXjRVRaPlEs+Ma9Kv/Xtxa4t9p/bf2poc7k0WcnGM6arLYDFuouXhiTNnSCCExdZTRv8eRojnedKfHkeH5dwN14sz4znOn5cfoFu7XBMGmDtpNuojhmZih95IPjcfNQDFqmRyKR7J6BER/lpofjx2iqguPHlDueg5WmxxffKnO73Ga+6tB0AgxVJ458IlXl7kqLsdwIF6fy3Fho8sZ8nXLLo+n4vD7fRFEEi02X5ZpDuRXyda/KzFCKmaE0D6oOlZbHRMFipenj+SFNN8ILYsIoRlEUnCjmVEojjEFV1d6sjuWGS9ML+Zu7q5i6xnDOAAX8KPE5zFZsKi2fuu1TtX1MXSWnKUzkU+RNnTPDmd7emJ7xtCYYySbGU8cPeWO+QdsL0TyVm4tNHlQdMqbGfN0DRWEsn7Shlls+8zUXhMJK3eXLb6+STxucHs4SC8FL1xb5+t1Vri82iTZZW/+xZ8a5enmSyzOFnoiotPw16+mTabKXpnK4YcxI1uTsSJbxQuqhW35XDHXnlKz14/RjbTC8vdIiEsnY+buVNu1tdsccBaSHQiKRnDQGRnyINf8XRO//a3khLTdAQ0FFEMURi3WXphsyk84lWQsv5PM3lnl7pcVkIYVlqISxYLbiMF1I8dZKExVBytJIGQphJLhTbmJpBg+qNnfLbequzzMTecptj3LL48xwhjgWGGpiem25AbOVNnEcc32hwULd4fZKm4ypcW40GeqVMlRKGZN8yqDc8kjlVN53YYRV22Op7uHHUHMCrpwqcnYkaUldmyXQVbUX0G+vtCikTZ6ZLHBjoUnd8SikdFRFJWVCFMW9IFd3gl6JotgyGc6a5FI6//tbc3zutUUqbb/vaz6et3jPmRL/z28/w6mRTM8oavshGVNPPCEKlPIpFuqJcfa954a37GpZaXrMVtrMVmxyHeNovzklG7+m5YVkTQ3HD7mz0mKx4ZExk4Fj3dfkqCI9FBKJ5KQxMO9io1kTVQEviDB1FVNTuL3SwvFDanbAX9+t0PZD4ijGCWLCGB5U24xmTEQsuLXcpOEkI9kVVeGJ0SxxJGh6QWfGh6BScyllLDRNwfUFz5/P8aBqs9KwUVSVt5YaDGVMsqaOqiikTI17lRafv7GIisLdSpuLkzmuL7Zw/YiFustT4znKTY+0ofHEWJbVts9q22eqaCUekSDiVCnDeC7Jtqy2k2mtd8sthBCb1t97O2DqLsM5k+dm8ui6yltLLQxNZbKYxtSSUed1J8AJI2pVn7vVNi+/schrc/W+r3PG1Hj36RKFlM77L4wkJt+OllhtB7y13ERXVTJmyPnRTFJSmKsBSelrKyHQLT/M1WyWGh7vOz+C44e9PTn9/BAb552AIBKCIIy4eGYIS1ePfCZBeigkEslJY2DER8tPTIyaphJGglvLbc4ttWi6AX4ckbU0VAWcIETXVDKKStsLaQUhFdsjpetoWYWv3K5ALKi2fSYLKeJYRwMqTjKiu5jRmClmSJsaNxdbrLY9hKICggdVl/G8RcrQaLshD6o283WHuh2QMXVmVx3ulluAgqKq1NoetpfmqfEc7zpdRAiB40dYusp43mKikOL1+QaGphHFCqqqEsRwu2yz1PQ5P+Lw3ExhXcdLNzBvDGijOZPRnMW9aRs3iCimDbwwWbq30nT54psrfPFWGdvvbx59/lSRD1wYZbJo0XRCFhsuLTeimDUopAwKaQMviBjJWp0SkE3bCzuGW7XXibKVEOiWH86NZFlqeNwtt8haOm0/pNKZ27Gxa2dtyeKVWQcUuDJTwvZjarbPzFDmyGcSpIdCIpGcNI72u+4eEkYxuqJiGipNL8APo15ASuvJnIzZio0QgjhOJoCWsiZjeQs/FKhGzFzNwQ9jRrIGyw2HlK4yXkjKIF4skj0vfkzKUDg7kuHGQgM3iPHDKNnFYvu4YUwQge35eCH4UUzDDQljQcpQWW37jORSmCoMZU1KGZ3zYznaXsjf3KtSd0LGCxagUrUDml4iFCrtNitNhzAUpHQFTVFYqjv4UcxozuoZFcfyFssNt2cI7XpDANKWzmQxTdCZ1fH735jjpWuLvL3S7vuarjWPjuSsXlml7Qc8GxcopnXGCylODSWG0DgWzNXcxLfgBizUHNp+yGo7YKnh9YagbaRbOqm0PFpeQCySLNCZ4QwAlbZP2tCTrh0/pO6EPVPm2pJF1tJRFHCCiCfGspwqpQli0fPx9NtxI5FIJJK9Z2DEh1AU2kFIzUnMjWpnjXzW0qk7PnNVlyiOKaZ0cpZG3fZQVQXXC1EUNemoQNByQ1RFIYxDIgH3Vl2cIMT1IyxDI5PSGS+kWWq4PKi5LDUdoliQs3SWmy5LNRcviomiCE3TMXUFEAgBKUNnOGMyVUwRA5dmimRNnbsd0+hiw2UobbLS9LB0BUXRqNk+rh/R8gIsTWGh6RJGSUfNRN4CNREJThD1JpR2DbYA50czvHBqCMtQ8cOYV+5Veem1Rf7y7U3W1hsqH336YfMogKlrPDlukbE0LF176GvXZltuLTV5e6VNKW0SxR4pQ+2Jo42tpUIIXptrEHZG2I/mrHVD0+pO2MkYwbmRLG4Qr5v10X3OrJmcqe1HnU4gl5evLRFEcW9PTHepn0QikUj2j4ERH0MpnTNDmU57p88z4zmemsiRMVT+8PUAQ4OJQoaK7UIMp4bzVNoeQ2mTJ8dzTBZTTJVStL2Ym0t1FDUZVOZ4EYaiUiya2F7IE6MZCmmdaw8aqCRzNBqOj6ooqIrSGyCWNlRs10dRFNKmwVhO5/xIlotTBaZLaSq2z1DGJIwEpqYxMZxiqeHgBTGWngwhWao7hGHMvbqDbmjMjKQRCkwULNp+hCLA7izGe2IsS9bUErNmuQ2CZPjWqo2Cwrce1PjD15c2nTz63HSBT16e5LueGSNjvvOfjWVoZE2NjKlj6uoa4eA8VOpZWz6otDwUBVodz0za0HqP3biFd+0QsRsLzXWln664KKZ1cqs2ThChq+q6WR+blSyuLzQIophnJgvcXGywssnPLpFIJJK9ZWDEx8xwloylUW575CydZ2dKXBjLsdxwiUl8Cy0/IKOr6JrGs1MF3lxuMV1M4YUxq22fsbzFqZEM96ptghgerDqM5kxOD6eZKWVZaXmcG8lTt5OFZm8vt0jpKilLQ1WS0e0tL0QBhEj+ZAwN01CZLqb59vMjFNIGuZSOqqqcHcmQtXTmqg6OHzKaS+GFEYaqcWOhgSKgmDGpOwGKgOtzDdKmzlguxXBHXEwW09wttzg9lKbc8vjy22XuVNpUOyWgctNndpOZHEMZg088N8mLz01yZiTT+3jK0MhaOllTQ9+wWXanMynODGcYy1m8udTC1DXqTsBK02O8kHqotRSSIWLdIWcCsW7mynghxVg+yYbsxpQ5lrcwNJWbiw0MTd2xkfO4Df2SSCSSo8bAiI+hjMFw1kRBYShrMJQxEEJwt5wsiHt6ssjdcotSNslgzNVdEIIYQcP1CcIAXVOYKaYYzaWYKiabbadLKZ6dKqBrGmdGMpwZTnN7pY2pKXhhRBDHuGHSYYNCMjysaLDUcAnjZOS7oqgIoOmFZC2dtKlza6mJ7YdcnMgxXUqRMjRGcsnOl7oT0nB8LEOnvGojOj/fYt1FU6GU1simTO6W2yzWHdKGynzN5fpCnesLTe5VbR5Uk+ffiKrA+y+McPXyJO87P9wTF5ahkTN1stbDgmMtO51JMV5IcXmmiKoonBvNYXtBr2vFDSI0lYeGiF1faCAQXJousFBz133vfhmO7URCd1Bcd15I9+/bcdyGfkkkEslRY2DER7nlY6galyZTVNoB5ZbPStPj2lydr9xexfEiRvMm33a6wHIz5P5qi9JQimrbp+FFGKrK7KrLWN5MfBZBTCmjM5y2uDxTZDSfwg0iHqzafOt+jbeW2zhBiO1HGLpO2gBT09EVge2HhJEgFIlZMmtqmFqW2ytNZistNEWl0k5KMm+vtHj+VIkPPzVGztJ57UGdVx9Uma+5uEHUK3fcXvEI4mR5W70dcGEiR97UqbkhigJvLrV45X6NtvdwtwokpZofemGa731ukuGsiRCCthchgoixnMVU8eFhX/3IWTqqAtfnG/hRxOnh9ENL6yARC2dHstSdRGy0/Qh71Wa1HaAqD++l6X59EAkWai6qAm4QcXultWn2Ybnh8sW3yrQ7ou7DT40yUUz3Pq+q6iN5POTQL4lEInk8BkZ8tPyI2dU2by0LTF2h5ScGzOWmlxhAhWCl5fHmso0XRlTskFJaoe4mQkE1FKpNH0NNzJxLTRcvilDVJu8+P8zZkSx/fnOFW8tNZqttaraLomgEUYgfBQShxpkRnZGswYOqg6qA0ekAUVW4W26TTRsoQDalM55NIVBIaSoLdYfrCw1GcyZemIxoF0DVSUaaq4qCpkAQCxpOSM32uFNpMzWc5fZKm6WGS58kB7qq8OR4jlOlFM9NF/jQU+NMldLkTJ22F3T2wfhcixu863SR0ZzVM2tuVmoYy1vMDKVZbnoYHVPvxiFg3YxE0w2YLqWw9KTLp9L2ewE9ZWhcGMs99L3XjkmfrzlEMZtmH+6t2twuJ6bWpWabsyOZdeLjUZFDvyQSieTxGJh3zayedGqkDAEoZPUkiEQi8Q+M5pP5FA3Hp5Sx8COXuZpDywuo2SGhEBgKCJGMaDd1jZypEYYxby81sTSVt5ZbeKHA1FTyaYsojrB9haxpoKjQdnxsBdpeQCTAjwSWQZINCULGi2liIAxibD8EVWHVgUDAfNXmz64vstL08aOYasvF9iJcPxnTPpzVCUOIEPihYKnpcnvV7ftaDGcMnpsu8MR4FjcQTOQthjMWpYzBTCkJzpW2R6XlJxt9mx4Nx2csnyKfMrYsNSiKQsrQGM1Zm2YG+mUkuntw+gX0jeWT86PZzth4ts8+CEHLDai2kzH0/bIwu0UO/XoY6YORSCS7YWDEx3zD537VIQgjDF1jru7x3CnBU2M55lZtQhGTtVQ0VaXcdhEiThbKiRjPC8hnLBw/YrnpE0YxdhDRdFSemcwTxYLZSpsgivCCiLYXMZY3SRsaQSjQVBUnCGj4EWEkaAcCtbM1FwGqquEGMTcWmxiawlPjOc6NZDg/lsPxQ+brLt+4t8qX3l7FDwIEKqoSE8egq2BoKi03pumFNDcpq6QNlclCiqfGs5wbzfKdT4zghYJr83WylkbGUqjZPssNl7F8Mm8jjAXljh/CCULaXsgzk4VNg/3aeRxNN2CuKtA19aHMQL+MxHvPDW8a0Pt5LHZS3jkznGGsYPHWUgtTV2k4Yc/U+jjIoV8PI30wEolkNwyM+Gg5yf6RXMqg5UXcXmryWilDPmXwzGSBO+Umrojxg5CaE7La9Ci3fMIwIhBqz4ugqQpDGYuFmgOKoNx0mV1tc2WmhK4qRCJmKGNQypiMZQ0Kpo5QBN+8X6PWDkBJulxQIGuq6EryNWlLx1RVdE3h3HCGc6N5zo9luTZX563lFq/P16l2JqE6XshkwSQSES1f4Ll+37KKQrJAbSht8MR4hiiC6WKaJ8fyjOQsri80MTWVuhOgKQq3lpp8fbbKE2NZnp8p9qaqmppGMa0DW5caugEojGMgEVjFdDKno3/G4Z1DbxXQ+3kszo9mNy3vrL2FzxTTqALOjeVx/FD6M/aJQfTByGyPRPLoDIz40DQVN4iStlTgTsVmZLHB5ekiThBSa4dkUjqrdtJ1EQlw/Ih82iCjC0xD5+JEnrurDgu1Nqqmkrd0QiFYqrt84IJGKWOSMTQ0XeX+qkPdCVht+9hewGo7wAsFWmf2VhRDGAsiBEMpEx2VfFrn9FAWOxAs1G1KWZ2m66OrKiqJr8P1wqQM0wz6DgGDpPPl0mQeVUlKLC0vwtRUsmmDyVIaUHh7pc2dik0pbTJfTwahWYbGzcUm9yo2TTfkQ0+O8r3PTfYd0NWv1NANQDOlDG/M11lueggU6k6D5zviApKMxBNjSVvsE7l3JpVuRj+PxVblnbW38JYXkk0ZuEHUNwsj2RsG0Qcjsz0SyaNz8t8hOuQsLTFlRhGKUHlQcygtNVmuu9yv2Sy3fZS2R932CWLBUMak6YbYboCS0snrGlOlNFHnFl9tezS9CMtQqbQDPn9zBU1Lbj2u3+lCMVRuLrrUbB87iPEFaCGogK5D1tQIo5hCSsdQFXKmThiFVG0PgWC17bHU8Li/2sYOIvwIunoj3iA8FMDSFdKmypmhFIoS86DqsdLyyJoG50czTA1lKTeTaaKlbFc8CAw92dJbq7vkUzpjOYuWF9L2Iy6M5XZ8g10bgMI4yZj0uwmPF1J8+KmxdaJmq66V7ZbjbQx4LS8kjGLSps5i3WaqmOaJ8Sz5lCH9GfvEIPpgBjHbI5HsFQMjPvwo8UboioodRFRaPoqi4ked0eaawkLTx/NjFGC17RPHMWbK5MxQGkPXuF+1qTkBQRTiBBG2FxDHOkEYU217xEKh6QaEsaCQMihmNFw/xPVjohgMBUwdcqaOH0Y4foShKjS8gFTH9/GgFhMJaLgRbhBStQNqTrhpliOlq6R1hVzaQEUgUDBUhXLDo+76DKsmURyhIMhbOmlD491nSgxnDJpuUoa4Ml1kopDi2lydpYZPGMfkLH1Xt9duaaVbZsmYKnfLba7PNxjKGuu+19oSy8Zppt3bY7+U9sZb5VaipOWFvNrZvJtLGeRThryV7iOD6IMZxGyPRLJXDMxvy2QhRSGVBNyMpZHteCeG0xaQbIv1ghARxxiGhqlDSjfRFMimDBwv5O3lFrGAphfR8kNQVYI4xgsFy81khLofx+QMFS8MURWdlKkTOyERSdZCF0lHSiAEiqoSxIJ6O8DV48QrISASgsW6R9DPyAGYmoKhJWUYRFIu6QZcXVNwQ0HNjQhDwartE0WCrGXx3nND627/3exDd6vt0xP5vgvn1gqBjJHMICm3/N5gLlVVWWl6vDbXWLe63tQ1gjhmupSIibXZDUiEx1duV7i/anN5poTb2T+zsXSyWUp7s4CXTDvN0PZDzo1ke3ttdhIYZR1fslMGMdsjkewVAyM+nj9V5MmJPOWmSySUXonCsnQadkS57dN2k3X1dSfE0FRGsip+DHfLNiCo2T5+lLSy+qFAU0E1FFRVQVMVbD/EDWKKqTRVx8cN2vhhjKUqGJogCKGYNtBUUIRC1jKoOT5tL8L2QxKb5ubkLY3xvImpCubrAYoiiBVI6wpPjWfJGhpjhRTLDYev3vZRhI4TRmiaQtPx8MKYC2uC6cbAPVFM952DsVYIzFVtHtQcTE3F0BRWO7M5Ki2PMIqZGcr0Vte/58ww8zUH2496wqQrJAC+dKvMq3M1lhs+y02PcyNZRnImOUun4fistnwKaZ3VVkDTDXacuVg/wCxet+tlO2QdX7JTBjHbI5HsFQMjPoQQaIogbapomsZQKln3fq/cRlGS9fVNJyCMY2IBfhjT8iKGcybDORMvCHEjHdf2CaJk/LeuAkIhFskI9XzKIGMKarZLww6xdQ1dVVA1BSUGK6Vj6Rq6qlJMw0ozER6h2Fp0aMB4wex10ay2fLzQRZB8XZRSGcqYvPfcMHUnxI8EU0NZHD+k6vhMFFL4QuEb92oPDfza7LXq3v67y+jmqjbnRnNU2x62H3Ll/CivzK7y9TsVLk2XaHkBQtDbFNz0Al65t0rGSDYE3686PDmew9TV3nbdlhcyXcxQsAzaXoAXRVTaPnUnJGWo3K/aBOVk4+zlU4Vd/Xs/6q1U1vElEolk/xkY8fGlW6tcm2/S8gW27+KHJpauEaHgRzFNN0BVQVNVYhEnu1bcEMvQyJsRVcdnteXiheBGHdOoItDUCF1X8IKYMAwYy5m4fjLEwwsjQgXiGDQ1CeqOH4CiUHUUVu1w0/OqQLzm/6+3fVRVSWaM+BG6Boam4YURuiK4W27TdHxUVSdjqgznDDK6xVJDxzR0hrNJCWknwXTt7b/pBjS9gJWmz1LTw9JUMqbOzcUGADnLZLqUZq4mGMmajOQsHD/k9blkS+zt5RY128cJY26ttPmOc0N829lhIDHc3l5JskPDWYOhtMlMKZMYVqOYU0NpihmDuh1g6Zvvk+nHo95KZR1fIpFI9p+BeWettpObfBTFhGFMw/F55d4quqIQxjG6ppCxDPwgJBaJWIiipIPEDnxqdoDtJ9NGIREGoQBVgCKSTa/VtocfJuZQL4SQ5AVWVYgjcMOktLKJlQNDhbGciROEeKHACZIx6kJNvlfD9rDyacJYkDZ1bC9CoODFgns1Fz8U6HqIisJoPsmELDY9FmoudSfgzHBmR8F07e3/lVkHFXjf+WHuVto8M54liGGuk+EwO4FaV1XOjmQZL6S4vdICFFKmloxR9wK+4/woc1Wb4azZy0Jcmiqw0vKIYkHG0LD9kFfurZKzdE4N5QljiGLBSM4inzIe9z+BHSHr+BKJRLL/DIz4yFgarh/RcEJUBTJW0t4qBLTdEFVRyFsqkW6wavsEEWga2F5A21WJomRo1tr6SAyggOsLWr5HDDhhIhi6FsWw98D+KCSiYyJvMl3KMl0weW2+TqXt4YeJwInj5CwxCrYXEouYnGUiOiompatoKuRSKl4I06UUFzoljmJK5+yFYaq2z9mRzKbBVAjBcsPl3qpN1fZpuiFzVUHW0lEUcIOYmVKGQsZivuYylLHQ1GS8+doFcJBkD/woYqXpMT2U5nY5Zq5qM5ZP8dREvuc5SZs6F0bzTJfSvD5fY7Xlk1VVhICRrMlYPnXgIkDW8SUSiWT/GRjxkdZVihmDKI5x/BglFgxlTW4tNWn5MQqCWIAiYrwoGQJmqMkW1VAEtHzoN7g8iNZ/XGz4334oJHNH8pZGLqXjBskQMCcIuVuNyKdMhKIShA6hEEQd8WJqCqqi4IQxvh2gq2oifkLBdN7E0HUMXaGUNdFVhUrL517NJlhq8uRYjoypcXulhRfGWPo7Jsy2H+EGEdce1Hh9oYkfRkyVUrz//AjvPlPqPSZn6TTdYJ0nYrMFcO85O4SiKIlAKabQNZXJYorhjEEcx5RbPpWWR8sLmKslP+NoLsWl6WR8ux3EXBjLSBEgkUgkJ5CBER81N9mEiqKgaoCqsdRwWWp4+EEIqElnhJ4ID0uHMAQniBEiyTyoAkTcyWZ06L9JpT+aAuM5k5mhNK4fsWr7VFo+KUNFNxVGcxblpkPDTXaQBHHyNSlTBSHQVRUhYnKmQSgEiqIylNYZypp81zNjeH7EqhOgK1BueRRTOkNpk8W6S7nl8ZW3KzhBRKUVMD2UIoxigjimmDKwg4h628f2IsI45s5Km7PDWc6N5h5qN93OE6EoCpemCox2hpW5QcRc1SEWcG2+yVTb58Zik6abmFRPD6U5M5xhrursaLHcXrW/yrZaiUQiORwGRnxoiKR8oUBK1xjL6ozmUlxfbOHHEESJyVQJk6xFEL6TvdB0hTgUGBqkLY2WHxHGSUlkJyhA3lI4PZTmyswQC/U25VZAGEUIoSDimIyhsep43Kt5tN2QSHRKNypkdY0nxrKMZC0WGzZeEOGG0PIDUobG6aEMxbTJfcem3g7ImyZLDYeVhodlaEwUUiw1HBbrHqM5izvlFi0/pNJ0qTo+lyYLNL2QKIyouhGmCnYY8+W3y8zXHT7y1BjPTiftsd1BYpCIhjiO+dqdCpDMBhkvpFAUZV354vZKMh+lmy25tdzi7ZU2pbRBzQkeEis7WSy32SAyRVF2LCr6bdft12p8EpHCSyKRHCb7Lj7+1b/6V3zmM5/hp3/6p/mt3/qt/X66TQmFgqIqiEhBILBMg7SukTVVwkglimIikqyGgN7MDdPQaDpRYvwUkDESw+h2wmOtPUQADU9QcxNDZdOLWG37aKpCxtQwNZ1YKMyuNGl7ggDIGgphJBjOGpwaypI2NFAE50ayrNrJXIwo1iikdKaKKYYyBvdXFZwg4nalSbXlk0/p2PWQhYZOFCXj2UtpAyeImK200RSFcjvgxmITN4iZKFogBLaf7ERZaflUnRARw1g+ac999UGdajvAjyK8MGax7nC7nAwmuzCa5SNPjz3UyruxgySlq7S9kCgSuGHUWzq3m8Vy3UFk37pf653nPWeHEhPrDmd19NuuOyjiQ84zkUgkh8m+io+vfe1r/Pt//+95/vnn9/NpdsRozuTscBpVVai1fJ4ZzzIznOHGcoNKO+g9risYuh5R14uISNpdgxhW7J0VWvppk1o7oKEEKEIQRRBGAiEiTK3blquQSavUnQg3EKQMldPDGZ4az3Kv6tByQxpxjKooZHQNXVHIGDpBFOMGMaeGUuiawqv3qzhBzFBGpenGNFyXS9NFluouAsGlqTw120dVVFbbLn4YoSgKOVNjOGNRSOu8Pl/HD5IZGw03GfKlKArVdrf11qPc8tA1hVLaABTaXv+tsRs7SJYbDpqiUHd8MqZO1tK3vIlvtcNl7XkURellT3Y3q2OHKawThJxnIpFIDpN9Ex+tVosf+7Ef4z/8h//Ar/7qr+7X0+yYpycLXJwqUm65pHSNQiZFzQk5M5xmvuqgqhG2J3qDu7qZC7ejQrZoWNkxdiDQVdDVJPuiqqAiODeSYyJvMF+1ieKYlAqWoXBqKM3FyRxhJFBQEMB83cMJIrxQEMUx8zWXmudj6skSt+limrYb8vZKCy+O0dRkr42uwJmRDO86M8ST4znemG/w9kqLcjuNIgSFjImhqWTMZIFete0zX7MJnWRr70Ld5anxXK+LZTRvIWJBKOJkcZ4fM1EwcYN3MhmbkTI0npnM92Z4pAxty5v4Vjtc1p5HV5XeY3Yyq2O323VPEnKeiUQiOUz27R3nn/7Tf8r3fd/38fGPf/xIiI+Lk3k+9swYX3pzmbIaoCoxD2ouhbSJaWi4YUxKT+ZzROzPXVgAKUPFC2IUBYopHV1TCcKQm8s+kUj2vxiawjOTBb7j/CiOH+JHUS9QmJqKZaq03YgoVnH9ZIdLue1xcSrPcDYpnSw3E8Fgd7pU2l7E82fyvP/CSM+X4QYRlqax0nQZzacYyeoM51PkTI1SSuftZQs3EsRRMsTsqfFcr4tFVxWGs0ZnwJjD2ytthtIG8zXnoSmqG4XFdCnFSM5aN8Njq5v4Vjtc1p6nO5p9p7M61m7XHbSZHnKeiUQiOUz2RXz8j//xP3jllVf42te+tu1jPc/D87ze3xuNxn4ciUo7YKnhUXUj7lZs3lpq4YUREwWLjKERhQHVcHfdKztBV96Z+WHqgEi8I5qazOfwY8H9mkvTC4hFMvVT1ZTET9FwCKLOsrlYkDVVTF2j5Qa9QWVDWZNC2iSKkyCd7DOJKGUSYTBftbl8qoSlqTw7XWQsb7HS9Fhpeli6zt+6VOLmYouJosVY3mK+5uBHoGkqxYyJ4oaMDlsYmkrbj7g4mQcSQdFdLJc2dYRQNk3hbxQWlq72DXy7vYlvZlTd6ayOQZ7pMcg/u0QiOXz2XHzcv3+fn/7pn+aP//iPSaW2N7B99rOf5Vd+5Vf2+hgP0fJCmo5PGMWEYYQXRBi6QiwEFTtgtR3vebZDAdKGStbUCUUMIqbuJGIijMAJIwytM8CMZEOuG8VcGMoyWUgjYoGuKuiail1zO2ZIgakpWGaM5wcMZ1MMZ3Umixa2F+KHcHY0x1LLJ2OqnB3NU0pbDOdMzo5kKbd8Xn1Qp9LyeFB1ABjOmVyaKnREAr1x6U+O51hp+UmWI2fgBhF/M1vl3qpN1tKZr7mM5qwtU/hCCNwgotzyqNk+Izmzt95+beDb7U18o0fk/Gh2z7s1HqUjRHaRSCQSyfYoQog9jbl/8Ad/wN/+238bTdN6H4uixNCoqiqe5637XL/Mx+nTp6nX6xQKu1smthXLDZf/9pVZPndtgZWmixfGKEpym98vFJLMh66CqatkLT3pclFU/DhGBXKWihsmy+wURSFraJwZSaNrGpCIo6ypUXVDzg6n8fwY01CIhYJKzGQpw1NjeVQVwlhwf9VGU1RsL+DpyTzPThdIGRp+JLB0ldW2T6Wzifb6fIPJYopLU4VeRuTVB3XCOKbthZweSpNLGVi6ihtEvDHf4F7FpuEFfOyZcbxQ8NREjvOj2U0D7nLD7duR8rgBebnh7nu3xmbPsZXAOIhzSSQSyVGk0WhQLBZ3FL/3PPPx3d/93bz22mvrPvbjP/7jXLx4kZ/7uZ9bJzwALMvCsva/3jyaM2k4PosNj6YbdbIc+yc8IPF4GFq3zKIwmjOoOwF2EKPRaeftzPNQFYWUoZJLJf4MQwddVXHDZIlcydJYWLVxIzA1gRcpXJzIkTZ1IhEjYpVLUwXm6y53VlqU0ib3qw6XT5UopM11i+IUBRZqLiM5i0tThYeMnbOVNi03ZLUd0HAjnj9VZLXtc6dioysKy02fa/N1Lk4WyVl6L4U/1gnKd8rtXlBuecmunO7k0pSh7Ukm4HG7NXaSodiqxXczgSG7SCQSiWR79lx85PN5Ll++vO5j2WyWkZGRhz5+UPhhzA/+v77EjcVm38+ryubL3h4XL0wyH6CwVPcIOrtfQhLRUXMTIaKoAkOLafsx1ZaLZVnkLZXxfIqxnMVK28cJIgKhMJ6zWLUDlhsOc3WX6UKKQkan7gbU2h6GqnJhLMs37lX50lsrvOt0iTBOdrPMVQUjuWT7bMZQWWm6XF9o9Pwb44Vkn8pqO2CqmOLGQpPrCw28ThdLNqUznrc4PZTh+VPFbYeBPWpXxVpxkDUTwdod8T6W37rUsxN2MudiqxbfzQSG7CKRSCSS7RmId0ZTV5NAukZ8KAqcHUozU0px7UGVur8/zx0BSpwsZnNFUl7ReKejptfWG4MTCMI4QFUUam0PP9QYz1tMFS2EIiikdG4utJit2qQMnSASNNwAQ4VVW8ULI0azaeaqdf7o+hJ+EJPSdXQ12WszX3PQNZUzwxkUReEb91b5ws0VhBBkTIP/x7fN8NxMqRdAbyw0uV+1EQi0zsZdO4iYLFo8Of7w2PV+Qfn8aPaRuirWioNutiZnGT2h8LjdGjvJUGzV4ruZwJBdJBKJRLI9ByI+vvCFLxzE02zJ3/22U3z+5gqFlM50McWpUporMwW+fq+KZRoofrBvo6YU9f/f3r3GRnqehf//PudnzuOZsb32eu095LTJJts2J9K0hR/tryiKKvpHKgUFKSW83IiECEQLQgGhNi0SCNRWoQWUvoCoVEBaqFRKaCH55y9C06RpkzbNaZM92Ls+j+f8HO//i8ee2rverHd37Nm1r4/kF+t4Pfezu5n78nVf93WBpqlu59T1eoZoJEcwfqQwSAIm09BpBjELrYCOH7PQ9MmnTQwN2n7EbD3CXr52G6gAXVfcOlHCDyOmai0GMykGcw6GlvS0KC8Xhyql+OGJKv/10xl+eKLGNcMZFlshb8w0uGF3sbuBvnKqljQlWz4yyacsppc6eIHihWOL3dsm79QM7GJvVawODl441gYNrhnO/yxQyLvn/L4bOVLZSIbina74SoAhhBAXb0dkPgDGBlL8P+8aYbLapuNHlLI2accgDGOCsPc3XVYL4yT4MEk6pa5kO1aCkIgkG5K2dUxdx48idJI5NKau0/ZCLF0niCNM3SRtQdY1mat7eH4EpqKcd4lijeeOLXLVYI7hostszeN0zWM4b5NxTPaW08w1kqFux+ZahJGiHUUcW2xRSttJC3d+tulCMtX3VLWTZE9SJtVmiB8FTFY76Mera3p6vNOmfL5jlHcKDjKOiaax4aOMjRypXEoA8U4BlbQtF0KI89sxwUe1FZBxTEaLKV49VWO+0aGStTF0DbXJNyEVEEWgG0mvD7U8GyaOwQZiDdKWRs41SdkG9Y5GJ1BEscILQ6ptDdMwcE0Tx9RJWQbtICJl6Zi6hqXrDGZt9gykMQ2N3UWXYtrmuN1iruExkLaZqibXaqeqHeYbHm/PJ8Perh7MEMYxB0fy3Lg7z/RSm+MLyayWZBBevhskKKV49XT9rI6i52sGBms35YaXTLPNudaGgoP1gpV3spEjlc3qcyEFp0IIcX47JvgYzDloaMzWPUoZl73lHK6lE8YKYnBN6ISb9/oRoMdgmRq+Ut20h6aDpSWb4UAm6cURxorp5Z/4XVNnsppcDR4ppsnYOqAxXfeoeyEjOZdSzqKUsRktpjF0jYYfgRbiR4pyxu0em8zWPcI4Zjjv8FbKxLV1rhvJkbYN3jNRQtd1/t/X5zg61wTgwGCG9189yP7BLJBkL9brKLoRa45RjrdBwbW78hsODlZnToB37J/Rz6JPKTgVQlzOLpdeRDvmnfG6XTl+6dAu/vuVaZa8EMsE0JKrnzpo0dpJtJshUKCHyd1aTQfi5EjGMnXKWYdrhnIM5Rx+OFmjEyoMU2EZGpap044Us/UOumaTsi0GUjYtPyKIIjJWilsnSly9K898w+v28Vhsecw12jz1WjLI7dDuAo1OwI/mWmhojORT7CmlGcjYlDM2DS+k6YUUUzaQTLY9M7NxcCRPOWN3syNKqfPOcoG1m3KSRdn4MQpc2HFGP2sypB5ECHE5u1yOhndM8KHrOndeVeHqoSzHF1ostnxeP12n6JrkXYuOn9RZbG7nD/BWrriQ1H+kzCQbkrYNsq6JZRg4JkxUMnQ8n6xroGkO4GHoGteP5JlrBNQ6AbFKqkdcy2T3QIq0pfP92Savz9Y5sdCimEpuxEzXOkmAk7EBDQO4aleeth8y2/BRaCy1a4wWXTKOyXR9OfORzZwVGGia1m3jHsWKpXaNm1bViKx2Zp3HyhHOhR6jwIUdZ/Szdbi0LRdCXM4ul6PhHRN8QLIxDBdSDBdSHJ1tcGy2xVzLp9ryCKLeTK7dqJXrtmgaWdfk0GiBMFa8MVOn4yssPcY0LPwwZqraJogUBdcin7LoBEm30qGcy/sOVNhdStHyI7718mn+960FOkHEfN3n5/aXKaQsUrYFKGYaSQATKcUPjlexdMVQIYVjuhxbaJF3Dd53VZmJcjLddbyUZjDnnJWmq3eCDf3jXS/CXjnCuVBynCGEEJfucnkv3bHv4FnHJIgj5hsekdLQDIXapLTHSk9XS0uai60c7RQdg8GcTSWXohUELC5FtPyIKI5p+klB6VAumbo7WrQJQsWppQ4TpRwHRwu8cqpOJWdTySZTaheaPmnbZKSQou2HOJaOrlvdGo6cYzCQtsk6Fv97dI6BlM3UYouTiy0yjkXGNtlbyXLrvnI34HhrrkkniJhcbCc9Span0m7kH+9KhL26WRm8c73GuchxhhBCXLrL5b10xwYfgzmHq4ZzVLIuS+2QxZa/JjDoJR1I2ckUW8swSDsGQQS7Cg6FlE055+CaBoNZjROLTRabAUN5F9cyuGY4w3R9hoWWz56BDKWMg2vr5FMmE+UUxbTNaNGllE6KTt+YbdL0Q3YXUlw1lKWSdbqZjLRt8Mqp2vL8FihlHbwowtJ0btlXpu3/rMZjddZirpF0TV0pXD3XVNozrdesLIjURZ0xynGGEEJcusvlvXTHBB/rVfgeHity4tpBDF3j9Zk6s3UffxPOXgwd0o5J3rUZyNhkXQPbSIpM867NRDnF6VqHU9UOjmWyp2QyNpAhXi7k3FV0aXoRrm0wNpDiht1Fml5I04twTIOpaodyxuauQ7vYXUzR8kPKWQfH1NE0jVv2ltA0DaUULT/i9FKHwZxDx48opi0qWYfppQ5+FDEepFEq6Sq60PDJp0xaXohr6d1Mx+qptO9UOb1es7JT1Y5cPxVCiB1uxwQf56rwvevQLmKlWGp7LDQ2p8d6J4bFVogXJHmVtq9zeGyAkYJLyjFRaFSbEQNph/GSwY1jRXblHabrPicWmhwaLXL1UJZjCy32VrIcHMnz1lyThWbAaDHF5GKL4wstylmHd40PoJTipckab862MPR291k1TWOinGGpHTDf8Aljxbv2FAB48cQSlpEEGJWsgxfGnFhsEczFWIbGwdEyo8XUWZmOd6qcXq9ZmdRrCCGE2DG7wLoVvnmXN+daPP3aHCcX2nQ28aqLF0GsIsKlNoW0w55SGscyQINi2saxdA7vKaJpGrsH0mQdk2MLHQzNoNb2mK557C6mmShn0DRtTdFQvRMwVW3R9mN0HfZXMiiS73NmQWiSjSiuyVS8NdekknXW/Nk4ps7YQIpC2mKplQyZW69Y9FJmpAghhNiZdkzwkbEN6p2AF44lzbtWrnv+9FSNU7UOhq6jNvGi7crslhiNThDy8lSVw2MD3c3dMnRq7ZDScuOulU392pEs1bZHre0zkLaI4xilFIM5hxt35zm+0GK61uanp+rEJMFAvRNQybpM1zprnhXWP+9bCWQmqy2aXsh8wyPjmJSyFguNgDBWeGHyusCaY5aMbVz0jBQhhBA7044JPpRS1L2kjiFWScOuph+haxooqHWCTXttjeTGi64nRadpy2ChGTDX6DCUT4a97R5IMZyzCWKotX1O1zxm6x2OLTR5c7bBUjPkx1N1TlZb3H3jKMOFFADHF1q8PZdMui1nHLKuSaQUXhSRNpKZKOezkpk4Nt+k0QmZb/hUWwEp2ySIPGzDYHIxOY4B1hyz3Lg7f96sxuXSUa+XzvVM2/FZhRCi13ZM8HFisc1s3aeYsnh7oUnbjzgwlCPrmOwpp5istjbldR0dbANs06SYMhjIpii4JinbZKrq0fAWObS7QDnrEMQ/m71yYrFF0bWZWWpxfK5FK4gwjWQs3Y27iwwXUhxfaPHmbJOMbWPqGn4UMWg7FFybgbTNSCHF2/NNji+0ujUf61nJTDS8sFtHMlVtE8WKwZy75kgFWHPM0vQj9g9m3zGrsbouRNdg90AK1zIuaXPu9yZ/rlqXy6V7oBBCXM52TPDxMxpBGBOrZAONoogB16KQstC9gGaPa04tA3Ipi8Gsy3glQyltstQOqbZCimmTVhiRT5lEserOXlEk11vHBlKYDYNWENIOFVoY0Q7ss14j65ocGMxy1WCGg6OF7pXa/31rAYCM3WKinDnnJriykc83PBpewGRVYeo6gzmHqWrnrCOVC21Q0/BCwjgmZRm8NFnljdk6+ypZTF3f0Oa8XqDR703+XLUul0v3QCGEuJztmOBjvJRmfyVD0wu5ejnjMbnY4vWZBscX2zS8iHaPA4+UDoWUTdY1qeQc2n5IZiDFnoEML5yo0gpCYi/i5GKbfZUsgzmHV07VeOVUjaV2yCun6mRsnfFShroX0vJDDgxmGS/9rAPpyjPdNFbk/VdXGC6kuldqm17E3kp2Tf+O9axs5GEUoxSUlwfcVbI2layzZtNXSjFaTH7CH8w5VLJnB0NnyjomTS/kRyeXWGz62KbG9SMFOkG8oc15vUCj35v8uboEXi7dA4UQ4nK2Y94Zh/IuH7hmcM2I9uMLLeqdgKYXAKqn7dUdHQayNq5toGsa842AXMqgE4SYhkPaNtCUzmInJI4iRosu1w5naXohjXbAe8ZLLDY9Rgou+wazTC8l11QP7U42Xq2W9OpYeabV9RY/u1Ib0lk+rkmGua1/VLGyka/cjilnnW4W4cxC0dm6x1S1QxQrpqodKqu+9lwGcw7jpTSNTsi1wzlePV3j7fkmu4vpDWdOzgw0+r3Jn+sGj9zsEUKI89sxwcdqmqYxmHNo+hHD+RRoGn6oejLVVlv+yDoGhg4Z28Q2dGYbPk0fdhVSvD3fZr4Z4FoG842AyarHD45XgSSbsTK0bayU4cbd+W6A0PaTbMjR2SYZx+xmOtb7iX+9TfBcRxUXspFfTMZhdTAURjH7B7NMlJNrwxvZnNdbX783+XPd4JGbPUIIcX47JvhYb+PN2AaoGFNPbrxcSuBhkPx+AzDN5FbLrnwquX0Swa6ChmsaXDWY4ehsgyCMyNgarqWhaXBioYVSiv97/fBZm6qmaQwqxZM/Ps0LxxYoL1+jnSinu7dezrSyCQ6umtEy3/AIo/is/h8XspGfGQhkbIOZWue8hZ/rvcZGC0TP9XtlkxdCiCvTjgk+1vuJPWMbVDshrmlQylosNgOiOBn+dqEikoyHroFlaBQyNsN5C0NPrrv6oYVtKF6erDHT8PCCpKdIyjKIlvt22IbRvT2yOmhYOTJ5c7bBfCvAjyFj6xta1+qgq+EFKMVZGY4L2cjPDASUUhsq/LyUYEECDSGE2F52TPCxXuq+4YWkLZNi2qbWDun4EV4UE4dcVP2HAjwFttJYaoccne9gahpoOkM5k9GBLKeqHTK2QccP8ZZnqziGhoqhmDa7AcGZmZpCyqSUcTi4K8fpWoddBbdbeLpmDWfUddQ7QTfoOrkYY2gajqVvuFj0TGcGAkdnG3K7QwghxAXZMcHHuY4WhvIucRQz1/QIY4UCTA38SziDSdtJP475WtJK3Y9DBtImKI2MbbLY8plvBgwv3yTZM5Ah5RiMldLddXXH0RddfjK5xFQ1ptEJyNgmN4zkuXlvicGcw0ytQ70T4IUxjqnjhfFyj47kSuxo0e0GXU0vIumppnWH0a3Uk1xsr4x+F34KIYS48uyYnWK91H1yW6TC88fmME5pFNM2Cy0fpV1aAUjHj3FsCGNoh0kOZSBjM1RwsU2NU0stXFPHMjWUgolKmoG0g2sZ3c1/ZVN/ZarGa9NJdgFNsavgcvPeCgdH8t3syELD58Rii7GBFEEUYxk6148WmKq2cUy9G3TNNzzmm343S3FsvsnxhTZNL1xTwHoh+l34KYQQ4sqzY4KP9WialtwWybo4poFr6UQNhXcJd241wLU0XNuk3vJZanaoZFOMFVwGUhauqRMpxbUjipmah2UktRsNL2C+4XU38NXj6OfqHqaho2ngWHo3SFnJjuRTJsFcTCFtUWuFBHG8nIkAL4zRVs1hWWqH3SxFtRVwdK5JMWUzXW++YwHrO/0ZSj2GEEKIC7Gjgw9IaiQqOZsYODHfuqjAY+V6rQ4MZC1GCy7HF9qEaFimST5l4VoGDS/EtXRswyCfsdhbzrK3ksE2NI4vtJlv+Cy1w27R5krh5mzd4+hcE4AD2cxZDa0WGslguqVWQCljd9uXd4KIycU2sWLdOSxvzzVW/hQu9Y9RCCGE2LAdH3zM1j1q7YCMpTN7EaNBdCBtQjFjY+ka+ZRFO4jwwhDLNBguOMSa4tXpOqWcw/+5Zghd09lVcDk4kmcw53B0tsHbc20AFho+9U7QDTwGcw7vv7rCRPlnXU3PbGhV7wQcGsvjmDo51+rWbhydbRArzjmHRSnFgcGkSPRANrNuAet6+j1XRQghxJVtxwcfDS+k4cWkbBNjg/unTnIbRie5WqvrGq5lMJx3STsGC3WfrGvR9CNmah7FtE3GsZip+bw8tcR1uwocHMl3AwwvjDmx2CKYS+o1Do3lu6+1cjS03nFI98jjHB1Gz1cMOpR3ef/VZ3dIPZ9+z1URQghxZdvxwUfWMSmkLWxTp5S2mGkEBOscvTgGWDr4Ed3/rgMo0DRFa3l+ymDO4WinSRgpMraJricNx0ppE8cy2DOQ5qaxwpqN3jF1xgZSFNIWS60Ax9xYD4/V1stGnK8Y9GLrNfo9V+VyI5kgIYS4MDs++Fg51piudbANsMwOUwsdVs+Yc3QwNFjuC4apJZkPDdB0UOjoukHLj5hv+lSyDo6h0w5ibFMj71poWlJz8XP7y2dlCXKuRTnrEMWKctYh51oX/BznykZsRjGoXK9dSzJBQghxYXb2rsHKnBeXG0YL2EYy46XW9qm1Y2LA1iHraJimRRQGBEqjnLZYaofJT7c6RJHCNXXKGZtSxibvmHz/+CILLR/X1Mm7FhPlLB+4ZnDdo41eXFfdymyEXK9dSzJBQghxYXZ88AHQ9CPyKZt9lSzPvDGLqeukHTCJqeRShLGi4Ue4joMWRliWQckwSdsGoVJ4QYhrGbi2yUDapuBa2IZGOeNQTJsMZByG8uef/qqUYq7hUe8EawpHNyJjG9Q7AS8ca5NZvlZ7IS7k6ECu164lmSAhhLgw8i5JsnnoGjz/9iIzNZ+2HxIBXgSq5RPHMZ1AYWoBGcfC0nUWOj6GBsMFF9twKGUcDo8XOVXtMFVtYxkGtgleqMi55jkDD6UUr5yq8cKxRdp+xFI7YLyUoZS1Lzh9ry3f+b2YcgM5Orh4kgkSQogLI8EHyeaxeyCFpilSjoEfxbS95NjFayWFHqYOKoZWEC0PhlNoeoRe9xgrpUk5Jg0/ZqHpE6MxUnCptyMqeYtfftdurtuVW/e1Z+sePzhe5eRiG12HejsknzKXB8GF3QFz58tINP2IrGNxzXC+e632QsjRwcWTTJAQQlwYCT6WNToBjp1cl52ve92rtCsXX1ScZBSiOKbRjglj6AQhnmty9XCWnG3S8QPKGYesa3Bioc3+QYtb95UZKbjMNfx1A4eGF2Isdy59a7aBqSfNwso5h4xtdLMitmEwkLE4vKfIUN4965gkYxuXlPqXowMhhBBbRXYYkuzDSyeXOLnYJghjUpaBF0WoVY0/bQNSto5jGTQ6EVqchCW6pnF0rokfKcZLaRabPoXAJOuajBZdXp+u8+ZMnaxr8b6rzp6dknVMTEOn2kqOdEaKLvsG0+ytZFFKdbMiqwfODXH2McmZ3UsvNPUvRwdCCCG2igQfQL0TMFv3QGlEMViGRtbWaHoKw4C0AWnXIu8apCyTMGqjoWOYGnnXwNI1IqXww5hjCy0Gsxa6pvP2XINaJ+TwniJH51qYusYdByprMiCDOYeJcpqmH7K3nKEdRFRyyRXZo7MNTF2jknOYrXs4pt7NSJx5THJm99ILJUcHQgghtooEHyQdRmfqHnMNj2o7IIyTNummEWGbBhoKXTcoZVwqWYe0Y1Ft+TS9iIGMy95KGpTG5FIHL1DU2xGzzTamBvPLTcNsy+DEYovMyaVuMefK0QkkGZB2EGHq+prZLeWsDUDKMnj3eLGbkdguxyTSoEsIIXaeK3PH6jHH1Ll2JEsrCKlP+cRKUfdCbN1IpsmicEydjGPSDmPGSylunhjgtdMNdhddRgopJpfazNTb5FydmXqHpU5IJWuDrjHX9Lh2OM+h0QJeqKh3AoDlkfYt0raBUlDO2EyUMwzmHJRSKKUopCwKKYvxUpqhvLsmY7Idjknklo0QQuw8Enyw3GE045BLWaRsg3onxNENlKbR8UM0NBpehGsZGLpOFMYsdUJcW+fweIm2H+K2OnhhzMnFFnEMYRyz2Ao4UE4zXs4wOpCiE8aYuo4Xxrx1conJxRbTdY/b95XQNZ1y1ulmRF45VeMHx6uYukY5a6Np2pqMwHY5JpFbNkIIsfNI8MFK3UWGRicgjhQdf57RgQzTSy2iGAopi5OLLeptD9M0GcraLDR8iimb548tEEYxS+2AxWZAy1egYoYLLjoag7kUVw9l2TeYxTF1NE2j0QkIo5i9lSzTdY+355vsLqa7RyezdY8Xji1ycrFN5YxC0+1muxwfCSGE2Dh5pyfJIkyUMxybb6HrGrmURcsPcSyDpU7IYquDbhgU0jZhrNH0Q1K2ybW78pxYbNH2Q47Pt1jyAkoZk4VWiGPq7C5lKKZNXMvi9JKHrkPWsWh4Qfcmzf5KholyunvcAkmgYRsGg8uFpinL6G7K261GYrscHwkhhNg4CT6Wrdw6aXQC9lUy/GSyRr3jJzNcDBOdENc2Gc6lqLY8YqWYqbeBmAOVLEpB/VQAWjL75cBgngNDGUoZh4OjeV44tgAaXDOcZ7KqKGdsylln3QAi65gMZJLhco6p8+7xIpWszUyt060TyTgmpq5f8TUS2+X4SAghxMbt+OBjdSYh45iMldJMLrYZr6SptU1OLnaoZG0ans5gxmZfJc3xeUUYa7SDCA2N6UYHLwwZK2UopUzuuKrC7ftKBDFMLraZqibzVjQNpqptTF1nopw5Z9AwmHM4vKe4JhuwUpi5uk6kE8Tb9jhGCCHE9rXjg4/Vty10DXYPpCikLOIpxWzNwzI1Zho+A2mT0YE0+yoZbCO5BaOUotr2abYDHMvi0O4ssVJcuyvPVcN5ppfanFxs0QkirtuVpZJ1aAXxeY8X1ssGrBRmnqtORAghhLhS7Pid68zbFq5lcHAkD4CmIO+avHhikV2FNLV2wGzDoxWETM60sUydnGOSTpn4NY9qy8dYDkpm6x7/35vzvDnbBCCIFAdHNFp+xHzDQym15urs+awUZrb9sFsnMl5Ko5Ti6GxjW9R/CCGE2Bl2fPBxrtsWmeW255apk3MtvChkZqbDG9MNXFvDNExGCjYjxRSVjM3r000Wmj67CikyjknDC2l4IcWUBWicXmozW+9Q95KBb/srGd5/dSW5/bKB4tH1CjOlR4YQQogr0Y4PPs61qU9V21iGTj5lMpR3ObXUIQZOLrYoZxwKGZ0g1Gh0QuYbHgXX5PrRArmUibt8OyXrmEzXksxHzjWJ4rgbjDS9kOMLLZba4YaCh3c6ipEeGUIIIa4kOz74WNnUV0bXvzXXZL7hEUaK60cLTFat5eyIznyjw3TNxLVNau2ArKMzqlLM1Dwsw2Cx5VPK2uRci8Gcw/uuqjBeSgOQXp5Qe3SuBSSZD+CSggfpkSGEEOJKJLvVstVHGCt9OFZuptw8USLjWLx2uka15SfHMYZiOO8yUnTohBH7KxnmGz6mrqGWm3gMF1IMF1IopZipdRgvpcm7FsW0xUQ5CT6W2rWLDh6kR4YQQogrkQQfy+qdIDk+SVsEUcz+SoZKziXrmFSyNoM5l7GCg9JgbqlD04/R0fjp6TqGrlPvhMw3fZSmCN9QvO+qCsOFFJAENi9N1gij5GrsQCZpl17J2pcUPEiPDCGEEFciCT6WJXNZ2hydbRJEMaW0zd5KtlsEOpR3OTbfxNQNBgspGnNNxkoZHENjpJii6QW8PlvHasNsw2PPQKobfKzUZqRskx9NLtH0Q5baITfuzsvtFCGEEDuO3u8FXC4cU2fPQJp9gxkipZhaavOjk0vdkfer2bpOGClOV9ukHZPdAynqnZDZus9M3Wem5lNtBd2vX6nNeHuuAcDecoYoVhxfaPGjk0u8Pt0452sJIYQQ241kPpblXItS1mZyMWldvrecYbrm8ZOpJeYaHrah0QkisrbOqaUOrqVjmzr1TsArp2rUOiEayRXdfEqjmLa633ulNqOQMskutGgHEaausdj0OVXrsLecoR1E1DtJwLJd5rYIIYQQ65HgY9mZAcLpWofXTjd4a66BHypGii5L7QADjWrLJ2WZVHIOLT/CMHQO7S4w2/DIOxYTlUy3oBRW3ajJOYyX0hxfaLHY9Dmx0GKu6TNd8zgwmMELY96Svh1CCCG2OQk+zlDK2GQck9dO14iUIo41JpfalDIWYaQYzNsstByyrsGx+RaupZNxTdpBxE1jRcZLayfUrqZpGpqmsdQOOVXrMN/0uW5XnmrLZ7yUxjF16dshhBBi25PgY9mZ3ULTjpl0OdU0dA1m6h5KwXxTp5Ay0TUdRchg3iXnWFSyTjfoeKejku6MluVjnWrLZ/dAupspkb4dQgghtjvZ3Zad2S10IG1xYDBDreVTzli0Oj6FtEspY1DOZjhVbRNEFtcMZemEMeWss6Ejku6MliDiwGDmrEyJ9O0QQgix3UnwsWwlKJhcbCW9ONImB0fynFho8tJUjXonRjdDFtom7aDN6ZrHTD0ZMnfTWHHDWYr1GoOtzpRI3w4hhBDbXc+v2j7yyCPceuut5HI5hoaG+OhHP8qrr77a65fpuUrWZrTo4oURdS9gvukzVe3QCWLSlkHGMXh9usbR2TphFDFacLl6MEvesRgvpTecpVgpPt0/mL2gqbZCCCHEdtHz4OOpp57iyJEjPPvsszz55JMEQcCHP/xhms1mr1+qp+YaPpOLbU4utHn1VJ3Zhs/kYhMviAmimDdmGzQ6IdVmQM2LWGoHhEp1b7ZIECGEEEJsTM+PXf793/99za+/8pWvMDQ0xPPPP88HPvCBXr9czzS8kMVmQBDHnFpq89Z8i5GCzY2jBfYMuJystnHzDn4YE0cR79pbYiBtX1DWQwghhBBbUPOxtLQEQKlUWve/e56H5/2ss2etVtvsJa2hlqfZzjc85psdWn7EYM7h+EKLrGMx3woopWxsQ2euHpBPmWQdh6uGcuwfzG7pWoUQQojtYFPbq8dxzIMPPsidd97JoUOH1v2aRx55hEKh0P3Ys2fPZi7pLCtXbOcaHkEUo1CkbINy1mEg7QAa5azF4T1Frh/JMZxzKWetS74GuzLp9uhsg5lapzsJVwghhNjuNjXzceTIEV5++WWeeeaZc37Npz71KR566KHur2u12pYGIN2hb5bBXNPDREMHhnMOjqmxq5Di6uEckQLT0DA0jX2D2W4r9Atpgb6SZWl4IZ0gYnKxTayQbqZCCCF2lE0LPu6//36++c1v8vTTTzM2NnbOr3McB8fpX81ExjZoeAHfO7rEyYU2E6U0xxfaDOcddE3j4EiecsYGGuQciyhWTNc6tPz4goOG1Y3M5hoelq5zcDQv3UyFEELsKD0/dlFKcf/99/PEE0/w3e9+l3379vX6JXpOKVAoFBrVdkC1HWIaBg0/ouVHtIKYnGvxnokShq7R9CNGiymiWNHwwg2/zupGZqau4UeRdDPdYnLcJYQQ/dfzHe/IkSM8/vjjfOMb3yCXy3H69GkACoUCqVSq1y93yZp+RM61+MA1Q0SvzlJvexTTFgMpi2j5a1YakE1V22QcE03jooKG1d+nnLUZKbi0/ORVlFIopeTK7iY7s42+HHcJIcTW63nw8eijjwLwC7/wC2s+/9hjj/GJT3yi1y93yVYCgk4Yc9NYgaxjMLnYpumHmIZO2jaoZO1uV9KMbQBJ0LK6Bfrqeo71OpfC2d1NlVK8NFkjihVL7Ro3LTcgE5vnzDb6ctwlhBBbr+fBx5WWxj4zIKhkbX56us4LxxaxDYOpaofBnHvetucb+Yl6pbvpyvc5OtuQjXCLrc4+yXGXEEL0x45/5z0zIABwLYPBnHtBQcHF/EQtG+HWW2+2jhBCiK0lu906LiYouJjfIxvh1lsv2BRCCLG1JPhYx8UEBRfze2QjFEIIsRNJ8LGOiwkKJJAQQgghNmZT26sLIYQQQpxJgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWuuwGyymlAKjVan1eiRBCCCE2amXfXtnH38llF3zU63UA9uzZ0+eVCCGEEOJC1et1CoXCO36NpjYSomyhOI6Zmpoil8uhaVpPv3etVmPPnj2cOHGCfD7f0+99OZDnu7LJ8135tvszyvNd2Tb7+ZRS1Ot1RkdH0fV3ruq47DIfuq4zNja2qa+Rz+e35T+sFfJ8VzZ5vivfdn9Geb4r22Y+3/kyHiuk4FQIIYQQW0qCDyGEEEJsqR0VfDiOw8MPP4zjOP1eyqaQ57uyyfNd+bb7M8rzXdkup+e77ApOhRBCCLG97ajMhxBCCCH6T4IPIYQQQmwpCT6EEEIIsaUk+BBCCCHEltoxwccXv/hF9u7di+u63H777Xzve9/r95J65umnn+YjH/kIo6OjaJrG17/+9X4vqaceeeQRbr31VnK5HENDQ3z0ox/l1Vdf7feyeubRRx/lpptu6jb+ueOOO/jWt77V72Vtms9+9rNomsaDDz7Y76X0xB//8R+jadqaj+uuu67fy+qpyclJfuM3foNyuUwqleLGG2/k+9//fr+X1TN79+496+9Q0zSOHDnS76VdsiiK+KM/+iP27dtHKpXiwIED/Omf/umG5q9sph0RfPzjP/4jDz30EA8//DAvvPAChw8f5pd+6ZeYmZnp99J6otlscvjwYb74xS/2eymb4qmnnuLIkSM8++yzPPnkkwRBwIc//GGazWa/l9YTY2NjfPazn+X555/n+9//Pr/4i7/IL//yL/PjH/+430vrueeee44vfelL3HTTTf1eSk/dcMMNnDp1qvvxzDPP9HtJPbO4uMidd96JZVl861vf4ic/+Ql//ud/zsDAQL+X1jPPPffcmr+/J598EoCPfexjfV7Zpfvc5z7Ho48+yhe+8AVeeeUVPve5z/Fnf/ZnfP7zn+/vwtQOcNttt6kjR450fx1FkRodHVWPPPJIH1e1OQD1xBNP9HsZm2pmZkYB6qmnnur3UjbNwMCA+tu//dt+L6On6vW6uvrqq9WTTz6pfv7nf1498MAD/V5STzz88MPq8OHD/V7Gpvn93/999b73va/fy9hSDzzwgDpw4ICK47jfS7lkd999t7rvvvvWfO5XfuVX1D333NOnFSW2febD932ef/55PvShD3U/p+s6H/rQh/if//mfPq5MXKylpSUASqVSn1fSe1EU8dWvfpVms8kdd9zR7+X01JEjR7j77rvX/L+4Xbz++uuMjo6yf/9+7rnnHo4fP97vJfXMv/7rv3LLLbfwsY99jKGhId797nfzN3/zN/1e1qbxfZ+///u/57777uv5cNN+eO9738t3vvMdXnvtNQB++MMf8swzz3DXXXf1dV2X3WC5XpubmyOKIoaHh9d8fnh4mJ/+9Kd9WpW4WHEc8+CDD3LnnXdy6NChfi+nZ1566SXuuOMOOp0O2WyWJ554guuvv77fy+qZr371q7zwwgs899xz/V5Kz91+++185Stf4dprr+XUqVP8yZ/8Ce9///t5+eWXyeVy/V7eJTt69CiPPvooDz30EH/wB3/Ac889x2//9m9j2zb33ntvv5fXc1//+tepVqt84hOf6PdSeuKTn/wktVqN6667DsMwiKKIT3/609xzzz19Xde2Dz7E9nLkyBFefvnlbXWmDnDttdfy4osvsrS0xD/90z9x77338tRTT22LAOTEiRM88MADPPnkk7iu2+/l9NzqnyBvuukmbr/9diYmJvja177Gb/3Wb/VxZb0RxzG33HILn/nMZwB497vfzcsvv8xf//Vfb8vg4+/+7u+46667GB0d7fdSeuJrX/sa//AP/8Djjz/ODTfcwIsvvsiDDz7I6OhoX//+tn3wUalUMAyD6enpNZ+fnp5m165dfVqVuBj3338/3/zmN3n66acZGxvr93J6yrZtrrrqKgBuvvlmnnvuOf7qr/6KL33pS31e2aV7/vnnmZmZ4T3veU/3c1EU8fTTT/OFL3wBz/MwDKOPK+ytYrHINddcwxtvvNHvpfTEyMjIWUHwwYMH+ed//uc+rWjzHDt2jP/8z//kX/7lX/q9lJ75vd/7PT75yU/ya7/2awDceOONHDt2jEceeaSvwce2r/mwbZubb76Z73znO93PxXHMd77znW13pr5dKaW4//77eeKJJ/jud7/Lvn37+r2kTRfHMZ7n9XsZPfHBD36Ql156iRdffLH7ccstt3DPPffw4osvbqvAA6DRaPDmm28yMjLS76X0xJ133nnW1fbXXnuNiYmJPq1o8zz22GMMDQ1x991393spPdNqtdD1tVu9YRjEcdynFSW2feYD4KGHHuLee+/llltu4bbbbuMv//IvaTab/OZv/ma/l9YTjUZjzU9Zb731Fi+++CKlUonx8fE+rqw3jhw5wuOPP843vvENcrkcp0+fBqBQKJBKpfq8ukv3qU99irvuuovx8XHq9TqPP/44//3f/823v/3tfi+tJ3K53Fn1OZlMhnK5vC3qdn73d3+Xj3zkI0xMTDA1NcXDDz+MYRj8+q//er+X1hO/8zu/w3vf+14+85nP8Ku/+qt873vf48tf/jJf/vKX+720norjmMcee4x7770X09w+W+NHPvIRPv3pTzM+Ps4NN9zAD37wA/7iL/6C++67r78L6+tdmy30+c9/Xo2PjyvbttVtt92mnn322X4vqWf+67/+SwFnfdx77739XlpPrPdsgHrsscf6vbSeuO+++9TExISybVsNDg6qD37wg+o//uM/+r2sTbWdrtp+/OMfVyMjI8q2bbV792718Y9/XL3xxhv9XlZP/du//Zs6dOiQchxHXXfdderLX/5yv5fUc9/+9rcVoF599dV+L6WnarWaeuCBB9T4+LhyXVft379f/eEf/qHyPK+v69KU6nObMyGEEELsKNu+5kMIIYQQlxcJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFvq/wfPqAyP2kEskwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# similarly, for fever\n", - "sns.regplot(x=\"new_cases_percent_of_pop\", y=\"search_trends_fever\", data=weekly_data, scatter_kws={'alpha': 0.2, \"s\" :5})" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "metadata": { - "id": "-S1A9E3WGaYH" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 63, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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HN57773tDqllHnzwwfD6669vdxjr5k/+5E/Cj33sY+0O4wNz7733hjt37gyDIGh3KC1x1113hQcOHFjysU9+8pPhPffc06aIwnBTVkYcx+HVV1/l9ttvb35MVVVuv/12fvKTn7QxMnEhCoUCAJ2dnW2OpPV83+d73/selUqFffv2tTucljp48CB33XXXkv8PN5Jf/OIXDAwMsGPHDu655x5GRkbaHVLL/Pu//zs333wzn/rUp+jp6eGGG27g7/7u79od1rpwHId//Md/5MCBAy29qLWdPvrRj/Lss8/y7rvvAvCzn/2MF154gTvvvLNtMV0WF+W12szMDL7v09vbu+Tjvb29vPPOO22KSlyIIAi47777uO2227juuuvaHU7LvPnmm+zbtw/Lskgmkzz11FNcc8017Q6rZb73ve/x2muv8fLLL7c7lHVx66238vjjj3P11VczPj7OF7/4RT7+8Y/z1ltvkUql2h3eRTt27BiPPPII999/P3/6p3/Kyy+/zB/8wR9gGAb79+9vd3gt9fTTT5PP5/md3/mddofSMl/4whcoFovs2rULTdPwfZ8vfelL3HPPPW2LaVMmI2LjOHjwIG+99daG2o8HuPrqqzl06BCFQoF//ud/Zv/+/Tz33HMbIiE5deoU9957L8888wzRaLTd4ayLxe8w9+7dy6233srw8DBPPvkkv/d7v9fGyFojCAJuvvlmvvzlLwNwww038NZbb/Gtb31rwyUjf//3f8+dd97JwMBAu0NpmSeffJLvfve7PPHEE1x77bUcOnSI++67j4GBgba9fpsyGenq6kLTNCYnJ5d8fHJykr6+vjZFJc7X5z73Of7zP/+T559/nsHBwXaH01KGYXDFFVcAcNNNN/Hyyy/z9a9/nW9/+9ttjuzivfrqq0xNTXHjjTc2P+b7Ps8//zwPP/wwtm2jaVobI2y9bDbLVVddxdGjR9sdSkv09/efkRjv3r2bf/mXf2lTROvj5MmT/M///A//+q//2u5QWuqP/uiP+MIXvsBv/uZvArBnzx5OnjzJQw891LZkZFP2jBiGwU033cSzzz7b/FgQBDz77LMbbl9+IwrDkM997nM89dRT/OhHP2L79u3tDmndBUGAbdvtDqMlPvGJT/Dmm29y6NCh5p+bb76Ze+65h0OHDm24RASgXC7z3nvv0d/f3+5QWuK222474zj9u+++y/DwcJsiWh+PPfYYPT093HXXXe0OpaWq1SqqunT51zSNIAjaFNEmrYwA3H///ezfv5+bb76ZW265hb/927+lUqnwu7/7u+0OrSXK5fKSd2HHjx/n0KFDdHZ2MjQ01MbILt7Bgwd54okn+Ld/+zdSqRQTExMAZDIZYrFYm6O7eA888AB33nknQ0NDlEolnnjiCX784x/zwx/+sN2htUQqlTqjvyeRSJDL5TZM388f/uEfcvfddzM8PMzY2BgPPvggmqbxW7/1W+0OrSU+//nP89GPfpQvf/nLfPrTn+all17i0Ucf5dFHH213aC0TBAGPPfYY+/fvR9c31lJ5991386UvfYmhoSGuvfZaXn/9df76r/+aAwcOtC+otp3juQR84xvfCIeGhkLDMMJbbrklfPHFF9sdUsv87//+bwic8Wf//v3tDu2irfS8gPCxxx5rd2gtceDAgXB4eDg0DCPs7u4OP/GJT4T//d//3e6w1tVGO9r7mc98Juzv7w8Nwwi3bNkSfuYznwmPHj3a7rBa6j/+4z/C6667LjRNM9y1a1f46KOPtjuklvrhD38YAuGRI0faHUrLFYvF8N577w2HhobCaDQa7tixI/yzP/uz0LbttsWkhGEbR64JIYQQYtPblD0jQgghhLh0SDIihBBCiLaSZEQIIYQQbSXJiBBCCCHaSpIRIYQQQrSVJCNCCCGEaCtJRoQQQgjRVpKMCCGEEKKtJBkRQgghRFtJMiKEEEKItpJkRAghhBBtJcmIEEIIIdrq/wHRnL6tjOlowAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# similarly, for bruise\n", - "sns.regplot(\n", - " x=\"new_cases_percent_of_pop\",\n", - " y=\"search_trends_bruise\",\n", - " data=weekly_data,\n", - " scatter_kws={'alpha': 0.2, \"s\" :5}\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Hd2A8707Uhz2" - }, - "source": [ - "We see that the slope of the line is positive in the graphs for cough and fever, but flat for bruise. That means that in places with increasing new cases of COVID-19, we saw increasing searches for cough and fever, but we didn't see increasing searches for unrelated symptoms like bruises. Interesting!" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Recap" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We used matplotlib to draw a line graph of COVID-19 cases over time in the USA. Then, we used downsampling to download only a portion of the available data, used seaborn to plot lines of best fit to observe corellation between COVID-19 cases and searches for related versus unrelated symptoms.\n", - "\n", - "Thank you for using BigQuery DataFrames!" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "9GIt_orUtNvA" + }, + "outputs": [], + "source": [ + "# Copyright 2023 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": { + "id": "h7AT6h2ItNvD" + }, + "source": [ + "# Use BigQuery DataFrames to visualize COVID-19 data", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
\n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"BQ\n", + " Open in BQ Studio\n", + " \n", + "
" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "n-MFJQxLtNvE" + }, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate creating line graphs from a ~20 million-row BigQuery dataset using BigQuery DataFrames. We will first create a plain line graph using matplotlip, then we will downsample and download our data to create a graph with a line of best fit using seaborn.\n", + "\n", + "If you're like me, during 2020 (and/or later years) you often found yourself looking at charts like [these](https://health.google.com/covid-19/open-data/explorer/statistics) visualizing COVID-19 cases over time. For our first graph, we're going to recreate one of those charts by filtering, summing, and then graphing COVID-19 data from the United States. BigQuery DataFrame's default integration with matplotlib will get us a satisfying result for this first graph.\n", + "\n", + "For our second graph, though, we want to use a scatterplot with a line of best fit, something that matplotlib will not do for us automatically. So, we'll demonstrate how to downsample our data and use seaborn to make our plot. Our second graph will be of symptom-related search trends against new cases of COVID-19, so we'll see if searches for things like \"cough\" and \"fever\" are more common in the places and times where more new cases of COVID-19 occur." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "ffqBzbNztNvF" + }, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [BigQuery COVID-19 Open Data](https://pantheon.corp.google.com/marketplace/product/bigquery-public-datasets/covid19-open-data). In this dataset, each row represents a new observation of the COVID-19 situation in a particular time and place. We will use the \"new_confirmed\" column, which contains the number of new COVID-19 cases at each observation, along with the \"search_trends_cough\", \"search_trends_fever\", and \"search_trends_bruise\" columns, which are [Google Trends](https://trends.google.com/trends/) data for searches related to cough, fever, and bruises. In the first section of the notebook, we will also use the \"country_code\" and \"date\" columns to compile one data point per day for a particular country." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nf__tMR-tNvF" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7_rsbkCktNvG" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XZKC6iMFxmMG" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "4aooKMmnxrWF" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pv5A8Tm-yC1U" + }, + "source": [ + "#### Set the region\n", + "\n", + "You can also change the `REGION` variable used by BigQuery. Learn more about [BigQuery regions](https://cloud.google.com/bigquery/docs/locations#supported_locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "bk03Rt_HyGx-" + }, + "outputs": [], + "source": [ + "REGION = \"US\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B9RWxD1btNvK" + }, + "source": [ + "Now we are ready to use BigQuery DataFrames!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wJ0gXezj2w1t" + }, + "source": [ + "## Visualization #1: Cases over time in the US" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "xckgWno6ouHY" + }, + "source": [ + "### Set up project and filter data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "-uiY0hh4tNvK" + }, + "source": [ + "First, let's do project setup. We use options to tell BigQuery DataFrames what project and what region to use for our cloud computing." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "R7STCS8xB5d2" + }, + "outputs": [], + "source": [ + "import bigframes.pandas as bpd\n", + "\n", + "# Note: The project option is not required in all environments.\n", + "# On BigQuery Studio, the project ID is automatically detected.\n", + "bpd.options.bigquery.project = PROJECT_ID\n", + "\n", + "# Note: The location option is not required.\n", + "# It defaults to the location of the first table or query\n", + "# passed to read_gbq(). For APIs where a location can't be\n", + "# auto-detected, the location defaults to the \"US\" location.\n", + "bpd.options.bigquery.location = REGION\n", + "# Improves performance by avoiding generating total row ordering\n", + "bpd.options.bigquery.ordering_mode = \"partial\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v6FGschEowht" + }, + "source": [ + "Next, we read the data from a publicly available BigQuery dataset. This will take ~1 minute." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "zDSwoBo1CU3G" + }, + "outputs": [], + "source": [ + "all_data = bpd.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9qV2y3iHp13y" + }, + "source": [ + "Using pandas syntax, we will select from our all_data input dataframe only those rows where the country_code is US. This is called row filtering." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "UjMT_qhjf8Fu" + }, + "outputs": [], + "source": [ + "usa_data = all_data[all_data[\"country_code\"] == \"US\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYCUayWkwq8c" + }, + "source": [ + "We're only concerned with the date and the total number of confirmed cases for now, so select just those two columns as well." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "IaoUf57ZwrJ8" + }, + "outputs": [], + "source": [ + "usa_data = usa_data[[\"date\", \"new_confirmed\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94oqNRnDvGkr" + }, + "source": [ + "### Sum data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "TNCQWZW83U0b" + }, + "source": [ + "`usa_data.groupby(\"date\")` will give us a groupby object that lets us perform operations on groups of rows with the same date. We call sum on that object to get the sum for each day. This process might be familiar to pandas users." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "tYDoaKgJChiq" + }, + "outputs": [], + "source": [ + "# numeric_only = True because we don't want to sum dates\n", + "new_cases_usa = usa_data.groupby(\"date\").sum(numeric_only = True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3jcwFPgK5BLh" + }, + "source": [ + "### Line graph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8GvJAgnH5Nzi" + }, + "source": [ + "BigQuery DataFrames implements some plotting methods with the matplotlib backend. Use `DataFrame.plot.line()` to draw a simple line graph." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "gFbCgfFC2gHw" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "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.6" + ], + "source": [ + "new_cases_usa.plot.line(\n", + " rot=45,\n", + " ylabel=\"New Cases\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sM5-HFDx70RG" + }, + "source": [ + "## Visualization #2: Symptom-related searches compared to new cases" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "se1b6Vf4XB9_" + }, + "source": [ + "### Filter data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wl2o-NYMoygb" + }, + "source": [ + "We're curious if searches for symptoms like \"cough\" and \"fever\" went up in the same times and places that new COVID-19 cases occured, compared to non-symptoms like \"bruise.\" Let's plot searches vs. new cases to see if it looks like there's a correlation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "olfnCzyg8jYi" + }, + "source": [ + "First, we select the new cases column and the search trends we're interested in." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "id": "LqqHzjty8jk0" + }, + "outputs": [], + "source": [ + "regional_data = all_data[all_data[\"aggregation_level\"] == 1] # get only region level data,\n", + "symptom_data = regional_data[[\"location_key\", \"new_confirmed\", \"search_trends_cough\", \"search_trends_fever\", \"search_trends_bruise\", \"population\", \"date\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b3DlJX-k9SPk" + }, + "source": [ + "Not all rows have data for all of these columns, so let's select only the rows that do. Finally, lets add a new column capturing new confirmed cases as a percentage of area population." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "g4MeM8Oe9Q6X" + }, + "outputs": [], + "source": [ + "symptom_data = symptom_data.dropna()\n", + "symptom_data = symptom_data[symptom_data[\"new_confirmed\"] > 0]\n", + "symptom_data[\"new_cases_percent_of_pop\"] = (symptom_data[\"new_confirmed\"] / symptom_data[\"population\"]) * 100\n", + "\n", + "\n", + "# remove impossible data points\n", + "symptom_data = symptom_data[(symptom_data[\"new_cases_percent_of_pop\"] >= 0)]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# group data up by week\n", + "weekly_data = symptom_data.groupby([symptom_data.location_key, symptom_data.date.dt.isocalendar().week]).agg({\"new_cases_percent_of_pop\": \"sum\", \"search_trends_cough\": \"mean\", \"search_trends_fever\": \"mean\", \"search_trends_bruise\": \"mean\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IlXt__om9QYI" + }, + "source": [ + "We want to use a line of best fit to make the correlation stand out. Matplotlib does not include a feature for lines of best fit, but seaborn, which is built on matplotlib, does.\n", + "\n", + "BigQuery DataFrames does not currently integrate with seaborn by default. So we will demonstrate how to downsample and download a DataFrame, and use seaborn on the downloaded data." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T9Hub_EAXWvY" + }, + "source": [ + "### Graph with lines of best fit" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "hoQ9TPgUPJnN" + }, + "source": [ + "We will now use seaborn to make the plots with the lines of best fit for cough, fever, and bruise. Note that since we're working with a local pandas dataframe, you could use any other Python library or technique you're familiar with, but we'll stick to seaborn for this notebook.\n", + "\n", + "Seaborn will take a few minutes to calculate the lines. Since cough and fever are symptoms of COVID-19, but bruising isn't, we expect the slope of the line of best fit to be positive in the first two graphs, but not the third, indicating that there is a correlation between new COVID-19 cases and cough- and fever-related searches." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "id": "EG7qM3R18bOb" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGdCAYAAACyzRGfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsz5JREFUeJzs/XeQZPd5341+Tu7ck/PMJgCLBbBYLLEgGCVSlEVRDKIYAPnVK8tSlZOqLMv0lSW6JNlyWWLJrlLR9vWVS657JbkcXoCkSNGiKVqiKAYxYReZ2AV2sWly6BxOPuf+caYbE3pmZ2Z7Znpmfp8qFLlzZrp/3bvTz/c84ftIYRiGCAQCgUAgEOwR8n4fQCAQCAQCwdFCiA+BQCAQCAR7ihAfAoFAIBAI9hQhPgQCgUAgEOwpQnwIBAKBQCDYU4T4EAgEAoFAsKcI8SEQCAQCgWBPEeJDIBAIBALBnqLu9wHWEgQBMzMzpNNpJEna7+MIBAKBQCDYAmEYUqlUGBkZQZY3z210nPiYmZlhfHx8v48hEAgEAoFgB0xOTjI2Nrbp93Sc+Ein00B0+Ewms8+nEQgEAoFAsBXK5TLj4+PNOL4ZHSc+GqWWTCYjxIdAIBAIBAeMrbRMiIZTgUAgEAgEe4oQHwKBQCAQCPYUIT4EAoFAIBDsKUJ8CAQCgUAg2FOE+BAIBAKBQLCnCPEhEAgEAoFgTxHiQyAQCAQCwZ4ixIdAIBAIBII9RYgPgUAgEAgEe4oQHwKBQCAQCPYUIT4EAoFAIBDsKR2320UgaBCGIYsVm6rtkTJU+tPGlnYGCAQCgaCzEeJD0LEsVmxenCrhByGKLPHwWJaBTGy/jyUQCASCu0SUXQQdS9X28IOQka44fhBStb39PpJAIBAI2oAQH4KOJWWoKLLETNFEkSVShkjUCQQCwWFAfJoLOpb+tMHDY9lVPR8CgUAgOPgI8SHoWCRJYiATY2C/DyIQCASCtiLKLgKBQCAQCPYUIT4EAoFAIBDsKaLsIugohLeHQCAQHH6E+BB0FMLbQyAQCA4/ouwi6CiEt4dAIBAcfoT4EHQUwttDIBAIDj/ik13QUQhvD4FAIDj8CPEh6CiEt4dAIBAcfoT4OKCIqRCBQCAQHFSE+DigiKkQgUAgEBxURMPpAUVMhQgEAoHgoCLExwFFTIUIBAKB4KAiItYBRUyFCAQCgeCgIsTHAeWoToWIRluBQCA4+AjxIThQiEZbgUAgOPiIng/BgUI02goEAsHBR4gPwYFCNNoKBALBwUd8cgsOFKLRViAQCA4+QnwIDhRHtdFWIBAIDhOi7CIQCAQCgWBPEZkPQUchRmkFAoHg8LPtzMc3vvENPvjBDzIyMoIkSXzhC1/Y8Hv/4T/8h0iSxKc//em7OKLgKNEYpb06X+XFqRKLFXu/jyQQCASCNrNt8VGr1Th37hz/6T/9p02/7/Of/zzf/e53GRkZ2fHhBEcPMUorEAgEh59tl13e97738b73vW/T75menuYf/+N/zFe+8hXe//737/hwgqOHGKUVCASCw0/bP9mDIOBnf/Zn+ZVf+RUefPDBO36/bdvY9hup9XK53O4jCQ4QYpRWIBAIDj9tn3b53d/9XVRV5Zd+6Ze29P2f+tSnyGazzf/Gx8fbfSTBAaIxSnuyP8VAJiaaTQUCgeAQ0lbxcenSJf79v//3/NEf/dGWg8YnP/lJSqVS87/Jycl2HkkgEAgEAkGH0Vbx8c1vfpOFhQUmJiZQVRVVVbl16xb/7J/9M44fP97yZwzDIJPJrPrvMBKGIQtli+uLVRbKFmEY7veR2sphf30CgUAgaB9t7fn42Z/9WX70R3901dfe+9738rM/+7P8/M//fDuf6sDRSdtY7+SlsROvjU56fQKBQCDobLYtPqrVKteuXWv++caNGzz//PP09PQwMTFBb2/vqu/XNI2hoSFOnz5996c9wKwcIZ0pmlRtb98swu8kFHYiJDrp9QkEAoGgs9l22eXixYucP3+e8+fPA/CJT3yC8+fP85u/+ZttP9xhopNGSO/kpbETr41Oen0CgUAg6Gy2HSHe9a53bauef/Pmze0+xaGkk0ZI7yQUdiIkOun1CQQCgaA1puNTtb19/4wWt6d7RCdtY72TUNiJkOik1ycQCASC1dieT77mYDo+urr/O2WF+DiC3EkoCCEhEAgEhwPXDyjUnI5bVSHEh0AgEAgEhww/CCnUHSqW15HWB0J8CAQCgUBwSAiCkJLpUjJdgg4UHQ2E+BAIBAKB4IAThiFl06NoOvhB54qOBkJ8CAQCgUBwgKlYLsW6i+sH+32ULSPEh0AgEAgEB5C645GvOTjewREdDYT4EAgEAoHgAGG50dis5fr7fZQdI8SHQCAQCAQHAMcLKNQdah02NrsThPgQCAQCgaCD8fyAQt2lYrn7fZS2IcSHQCAQCAQdiL9ibLYTvTruBiE+BAKBQCDoIMLwDdFxEMZmd4IQH3tAGIYsVuxVu1IkSdrvYwkEAoGgwyhbLsWaixccvAmW7SDExx6wWLF5caqEH4QossTDY1kGMrH9PpZAIBAIOoSaHY3NHiSvjrtBiI89oGp7+EHIcFeMyzNlLs+WAUQGRCAQCI44luuTqznYB3hsdicI8bEHpAwVRZa4PFNmqmAiIeH6JZEBEQgEgiOK7fkUai515+CPze4EIT72gP60wcNjWS7PlpGQuH84zWzJomp7Ym29QCAQHCFcP/LqqFpHU3Q0kPf7AEcBSZLoTxv0pw3cIODybBlFjjIiAoFAIDj8+EFIrmozVTCPvPAAkfnYMxYrNtMFE02WcXyfka44/Wljv48lEAgEgl3koKy432uE+NgjqrZHEMKZkQwzRZOYphzIZlMxNiwQCAR3JgxDypZHsX4wVtzvNUJ87BGNptOZookiSwe25CLGhgUCgWBzqrZH4QiNze6EgxkBDyCNptOVGYODSGNseKQrzkzRFE2zAoFAsIzp+OTrR29sdicI8bFHSJLEQCZ24AP1YcngCAQCQbuwXJ9C3cF0hOjYKiJyCLZFJ2dwRD+KQCDYS1w/oFBzqB6CFfd7jRAfgm3RyRkc0Y8iEAj2As8PKJouFcs7dNtm9wrh8yE4NKzsR/GDUNyNCASCthIEIfmaw1TBpHwI19zvJSLzITg0iH4UgUCwG4RhSNn0KJpibLZdiE9nwaGhk/tRBALBwaRiuRTrrhibbTNCfAgODZ3cjyIQCA4WdSdace94QnTsBkJ8CAQCgUCwjOX65GsOlvDq2FWE+BAIBALBkcfxom2zNdGovicI8SEQCASCI4vnBxTqLhXL3e+j7All0+VLL81yY6nOf/k7j+6bF5IQHx2MMM0SCASC3cEPQop1h/IR8eqYK1l89tIU//vlWSw36mP5/o08j5/s3ZfzCPHRwdzJNEuIE4FAINgeYfjGivujMDb72nyFp56Z5OuvLbL25f7//uaGEB+C9dxpiZtw9BQIBIKtU7ZcijUXLzjcEyxhGPL9m3mevjjFc7eL665n4xp/923H+TtvPbb3h1tGiI8O5k6mWWLDrEAgENyZmh2NzR52rw7XD/jalQWeujjFjaXauuvD2Rgff3SMDz0ywj0D6X044RsI8dHB3Mk0Szh6CgQCwcZYrk+udvhX3Fdtjz97cZY/eXaKpaqz7vrpoTRPXhjnnff2ocgSurr/m1VEtOpA1vZynOhLtuzlEI6eAoFAsB7b8ynUXOrO4R6bXazYfO7ZKb704iw1Z73AesvJHp58bJyHR7Md1w8oxEcHstVejt129BQNrQKB4CDh+pFXR9U63KLj+mKVpy9O8dUrC+uaZjVF4kfPDPLxC2Mc703u0wnvjBAfHUin9HKIhlaBQHAQ8IOQQt051CvuwzDkuckiTz8zyfdvFtZdTxoKHzo3wkfOj9Kb6vws+LYLP9/4xjf44Ac/yMjICJIk8YUvfKF5zXVdfvVXf5WzZ8+STCYZGRnh7/ydv8PMzEw7z3zo6ZReDrGiXiAQdDJBEFKoOUzm64d2xb0fhPzVlQX+4X97lv/XZ15cJzwG0gb/6F2neOrvv4W/986TB0J4wA4yH7VajXPnzvELv/ALfOQjH1l1rV6v8+yzz/Ibv/EbnDt3jkKhwD/5J/+ED33oQ1y8eLFthz7sdEovR6eIIIFAIFhJGIaULY9i/fCuuDddny+/NMtnL00zV7bWXb+nP8WTj43xw/f1oyr730C6XaTwLqSiJEl8/vOf58Mf/vCG3/PMM8/w5je/mVu3bjExMXHHxyyXy2SzWUqlEplMZqdHaxtHue/hbl/7UX7vBALB7lC1PQqHeGw2X3P4/HPTfPGFGSotelcuHOvmycfGedNE144/T3VVZqw7cbdHXcd24veu38qWSiUkSaKrq6vlddu2sW27+edyubzbR9oWR7nv4W4bWo/yeycQCNqL6fjkavahXXF/O1fn6UuT/MUr87j+6pyAIku8+3Q/T14Y59RAap9O2F52VXxYlsWv/uqv8rf/9t/eUAV96lOf4rd+67d28xh3Rac0fx5ExHsnEAjuFsv1KdQdzBajpAedMAx5ebrMUxcn+fbruXXXE7rC+88O89E3jR66G7ddEx+u6/LEE08QhiG///u/v+H3ffKTn+QTn/hE88/lcpnx8fHdOta2EX0PO0e8dwKBYKc4XkCx7hzKRnc/CPmb15d4+plJXpmtrLvem9L56PlRPvDwCKnY4fzc3JVX1RAet27d4q/+6q82rf0YhoFhdG53bjubP49aD0SnNM4KBIKDQ2PFfdU+fGOztuvzlVfm+eylKaYK5rrrx3oTPHFhnPfcP9ARLqS7SdvFR0N4XL16la997Wv09u7Pxrx20arvYaci4qj1QOy2CZpAIDg8BEFI0XQpmy7BIRMdpbrLn74wzReem6FouuuunxvL8uRj47z5RA/yIb4hXcm2xUe1WuXatWvNP9+4cYPnn3+enp4ehoeH+djHPsazzz7Ln/3Zn+H7PnNzcwD09PSg63r7Tr6P7FREiB4IgUAgWE0YhpRNj6J5+MZmZ4omn7k0xZ+/PIe9plFWluCH7u3nicfGuH9o/yc795pti4+LFy/y7ne/u/nnRr/Gz/3cz/Gv/tW/4otf/CIAjzzyyKqf+9rXvsa73vWunZ+0g9ipiNitHoijVs4RCASHg4rlUqy7h25s9vJs1ET6ratLrNVTMVXmxx8a4mOPjjHSFd+fA3YA245+73rXuzatwx22Gl0rdioidqsH4qiVcwQCwcGm7kQr7g/T2GwQhnzvep6nLk7y4lRp3fWuuMZPnR/lQ4+MkI1r+3DCzuJwttHuMjsVEbvVAyHKOQKB4CBguT75moN1iFbcO17AVy/P8/TFKW7l6+uuj3XHeeLCGH/rzCCGpuzDCTsTIT6W2U7pYqciYqflkTv9nBhpFQgEnYzjRdtma4dobLZiufyvF2b5k+emydecddcfGM7w5GPjvO1UL4osyuBrEVFqmb0oXez0Oe70c1vNxIjeEIFAsJc0xmYr1voJj4PKfNnis5em+N8vzWGuyeBIwNtO9fLkY+M8NJrdnwMeEIT4WGYvShc7fY47/dxWMzGiN0QgEOwFfhBSrDuUD9GK+2sLVZ56ZpKvvbqwrolUUyTe+2DURDrR0/6dKYcRIT6W2YvSxU6fo11nE70hAoFgNwnDkJIZTbAcBq+OMAy5eKvA089Mcul2cd31dEzlQ+dG+Knzo/QkD4eVxF4hxMcye+HGudFz3Kkc0q6zid4QgUCwW5Qtl2LNxQsO/gSL5wf89WuLPPXMJK8v1tZdH8rE+NijY7zv7BBx0US6I0T0WWYv3Dg3eo47lUPadTZhdy4QCNpNzY7GZg+DV0fd8fjSi7N87tlpFir2uuv3DqR48rFxfvi+ftFEepcI8dEB7EU5RDSbCgSCdmK5Prmag30IxmaXqjZ/8uw0/+vFGWr2+tfz5hM9PHlhjEfGu8TnZpsQ4qMD2ItyiGg2FQgE7cD2fAo1l7pz8MdmbyzVePriJF+9vIC3potUlSXec2aAJy6Mc6IvuU8nPLwI8dEB7EU5RDSbCgSCu8H1I6+OqnWwRUcYhrw4VeKpi5N893p+3fWkrvCBh4f5yJvGRGl6FxHiowPYi34T0WwqEAh2gh+EFOoOlQM+NusHId+8ushTF6d4da6y7npfSuejbxrjAw8PkxSfj7uOeIePCJtlV0Q/iEAgWEsQRGOzpQO+4t50ff785Tk+e2mK2ZK17vrJviRPPDbOu0/3oynyPpzwaCLExxFhs+yK6AcRCAQNwjCkbHkU6wd7xX2h7vCF56b50+dnKLcoFb1poosnHxvnwrFucbO1DwjxIRD9IAKBAIg+CwoHfGx2qlDnMxen+Mor8+u25soSvOv0AE9cGOO+wfQ+nVAAQnwIiPpBZAkuz5RxfJ/xnjhhGIq7AYHgiGA6PrmafaBX3P9gpsRTz0zxN9eWWJuviWkyP3F2mI+9aYyhrMjqdgJCfOwSB6mPoj9tMNodZ6FioykyM0WTvpQhSi8CwSHHcn0KdQfTOZheHUEY8u1rOZ66OMkPZsrrrvckdT5yfpQPnhsmHdP24YSdRWPYoBPeCyE+domD1EchSRIxTaEvZXRM6eUgiTeB4KDheAHFukP1gK64d7yA//PKHE9fnGKqYK67PtGT4IkLY/zomUF09Wg3kUqSREJXSBkqCV3pmM9RIT52iYPWR7GTUdzdFAgHSbwJBAeFxor7qn0wx2bLpsufvjDDF56bplB3110/O5rlycfGeMvJXuQOCbL7habIZGIaqZjakVbwQnzsEp3iq7FVgbATo7OFssW3ri01f+Yd9/QxmI235dwHTbwJBJ1MEIQUl8dmD6LomC2ZfPbSNF9+aRZrTV+KBLzz3j6efGycM8OZ/TlghyBLEklDJR1TiXX4wjshPnaJTlnittUMwk6Mzm7n67y+WKMrrjFfrjHRk2ib+OgU8SYQHGTCMKRsehTNgzk2++pchacvTvL11xZZe3xdlfnxB4f4+KNjjHa353PnoBJfLqukDLVjyip3Qnyi7xJ74Vq6FfYmg9D+f+ydIt4EgoNKxXIpHMAV92EY8r0beZ6+OMnzk6V117NxjQ8/MsJPPjJCV0LfhxN2Bqosk46ppGLqgTRHE+KjBYep2XE3MwgTPQlO9iWp2R4n+5JM9CTa9tidIt4EgoNG3YlW3B+0sVnXD/jq5QWevjjJzVx93fWRrhgff3Sc9z442PElhd1CkiSSukI6phHXD/Z7IMRHCw5Ts+NuZhAGMjF+6L5+kZ0QCDoAy/XJ1xysA7bivmp7/NkLM3zuuWlyVWfd9TPDaZ68MM7b7+nryMbJvcDQorJK2lCRD8l7IMRHCw5Ts+NuZhBEdkIg2H8cL9o2WztgY7MLZYvPPTvNl16apd7CZ+StJ3t58rExzo5mD2zm+W5oZKpTMRVDPdhZjlYcGfGxnVLKVkoVe1WaOUwlIIFA0D48PyB/AFfcv75Y5emLU/zVlYV1TbCaIvG3zgzy8QtjHOtN7tMJ9w9JkohrCulYZ3ly7AZHRnxsp5SylVLFXpVm7vQ8QpwIBEcLPwgp1h3KB2jFfRiGPHu7yFPPTHLxVmHd9ZSh8qFzw/zU+VF6U0evfKspy82jhop6AJtHd8KRER/bKaVspZywV6WZOz3PYepPEQgEGxOG0Yr7Yv3grLj3g5C/fnWRpy5Ocm2huu76QNrgY4+O8RNnh0joRyYcAQfLk2M3ODJ/2+2e+tgrH4o7Pc9h6k8RCAStKVsuxQM0Nms6Pl96aZbPXppioWKvu35Pf4onHxvjh+/rPzJ3+g1iy2WVg+TJsRscGfHRrqmPRpmjYrmMdMUwVJl0TNu1SY87nVuYcQkEh5eaHY3NHpQV9/maw+efm+aLL8xQadGLcuFYN08+Ns6bJrqOVOBVZZlULMpyHERPjt3gyESqdk1m7HWZ407n7gQzLtF3IhC0F9Pxydcd7AMyNnsrV+MzF6f4i8vzuP7qkpAiS/zI/QM88egYpwZS+3TCvecweXLsBkdGfGyHzYJpp5U5OmHctZUg608bQpAIBNvE9nwKNZe60/kTLGEY8tJ0iaeemeI713Prrid0hfefHeajbxo9Un1oDU+ORlZa0BohPlqwWXajU8sc+5l9aCXIANEIKxBsEdePvDoOwtisH4T8zbUlnro4yeXZyrrrvSmdj54f5QPnRjrm83G3OeyeHLvB0fiXsU02y250QpmjFfs59dJKkHVahkgg6ET8IKRQd6gcgLFZy/X5yg/m+eylKaaL5rrrx3sTPHFhnPecGTgyfQ0JXT0Snhy7gRAfK2hkD3LVqKF0uhCiKvIq9d4JZY5W7Gew30iQyRJcninj+D7jPXHCMBS/oAIB0Yr70vKK+04fmy3WHb7w/Ax/+vwMJdNdd/2R8SxPXBjn8RM9R+L3+yh6cuwGQnysoJE98IIASYrSh8d6kx2T3diM/SwHtRJk/WmD0e44CxUbTZGZKZr0pQxRehEcacIwpGx5FOudv+J+umDymUtT/PkP5tYtqZMl+OH7+nniwjinh9L7dMK946h7cuwGQnysoJE9GO1KMFM06T1AwbLTykGSJBHTFPpShii9CAREny+FAzA2e3m2zFPPTPLNq0uslUcxVeZ9Z4f52KOjDGfj+3K+vaThyZHUD89Ct05BiI8VdGoz6VboxHLQZu+nGM8VHBUOwor7IAz57vUcTz0zxUvTpXXXuxMaHz4/yofOjZCNa/twwr1DeHLsDQcnuu4B7cgeiKD6Bpu9n8IWXnDYsVyfQt3BbLGxtVNwvIC/vDzP0xenuJ2vr7s+1h3niQtj/NgDQ+jq4Q3EDU+OVEw9cjbv+4V4l1fQjuzBToPqYRQtm72fYhpGcFhxvIBi3WmOnHciFcvlf70wy588N02+5qy7/tBIhicujPO2e3qRD/jn0Gboyw7VwpNj7xHio83sNKgetUzAQS5xCQSt8PyAQt2lanfu2Oxc2eJzl6b40kuzWO7qMpAEvP2ePp58bIwHR7L7c8A9QJHfaB4Vnhz7h/jEbzM7Dap3kwk4iFmTTmuQFQh2ShCEFJfHZjtVdFydr/D0xSm+9uoCa4dsNEXivQ8O8fFHxxjvSezPAfeAhB6ZgCWFJ0dHsG3x8Y1vfIN/9+/+HZcuXWJ2dpbPf/7zfPjDH25eD8OQf/kv/yX/5b/8F4rFIm9/+9v5/d//fe699952nrtj2WlQTeoKVdvl2dsmKSP6BdkqBzFr0okNsgLBdgjDkLLpUTQ7c2w2DEMu3irw1DOTPHu7uO56Jqbyk4+M8OHzo3Qn9L0/4B4gPDk6l22Lj1qtxrlz5/iFX/gFPvKRj6y7/m//7b/lP/yH/8Af//Efc+LECX7jN36D9773vbzyyivEYp0dENvB3QTVMATC5f/dBqJ/Yv85iNknwc6pWC6FDl1x7/oBX7uywNMXp7i+VFt3fTgb42OPjvHjDw0RP4SeFbIkkTAUMjFNeHJ0MNsWH+973/t43/ve1/JaGIZ8+tOf5td//df5yZ/8SQD+63/9rwwODvKFL3yBn/7pn7670x5AWgUlYN3Xao5POqZxeijDTNGkto0OedE/sf8cxOyTYPt08thszfb4sxdn+ZNnp1ms2uuunx5M8+RjY7zz3v5D2VwpPDkOFm2NUjdu3GBubo4f/dEfbX4tm83y+OOP853vfKel+LBtG9t+4xelXC6380j7TqugBOuXrt2NgBD9E/uPyD4dbizXJ19zsDpwxf1ixeZPnp3iz16cbXnT8viJHp58bJxzY9lDl41reHKkDPVQjwIfRtoqPubm5gAYHBxc9fXBwcHmtbV86lOf4rd+67faeYyOYqONr2u/dqIvuWMBIfon9h+RfTqcOF60bbbWgWOzN5ZqPH1xkq9eXsBb03OiyhLvOTPAExfGOdGX3KcT7g6SJJHQleWFbuL37KCy739zn/zkJ/nEJz7R/HO5XGZ8fHwfT9ReNgpKa78mBMTBRmSfDheeH5DvwBX3YRjy/GSRpy5O8f0b+XXXk4bCBx8e4SNvGqUvdbj+DeqqTNrQSMWEJ8dhoK3iY2hoCID5+XmGh4ebX5+fn+eRRx5p+TOGYWAYh+uXZCUbBaW1X9tJw6JocuwchHg8HPhBSLHuUO6wFfd+EPL11xZ5+uIkr81X113vTxl87NFRfuLsMMlDlHUTnhyHl7b+Kz1x4gRDQ0N89atfbYqNcrnM9773Pf7RP/pH7XyqtrHbAXyjoLT2awtla9sNi1E/SZFc1cELQs5PdHFmOLPt8wsRIzjqhOEbK+47aWzWdH2+/NIsn700zVzZWnf9ZH+SJy+M8+7T/YdqlFR4chx+ti0+qtUq165da/75xo0bPP/88/T09DAxMcEv//Iv82/+zb/h3nvvbY7ajoyMrPIC6SQ6ZUphJw2LVdsjV3Wo2B5LFZswDHe0tr5T3gOBYD8oWy7FDhubzdccvvD8NF98foZyi9LPoxNdPPHYOBeOdR+a4Cw8OY4W2xYfFy9e5N3vfnfzz41+jZ/7uZ/jj/7oj/jn//yfU6vV+Pt//+9TLBZ5xzvewZ//+Z93rMfHXk8pbJRl2EnDYspQ8YKQpYpNf9pAV5QdnV9MagiOInUnEu+dtOJ+Ml/nM5em+MoP5nD91RkYWYJ3nx7giQtj3DuY3qcTthfhyXF0kcJOKmwSlWmy2SylUolMJrPrz7eTcsduPN92Sx9hGLJQtnhhqsjrC1V6EgY9KZ1z413bPv9evwcCwX7SiWOzL0+XeOriJN++lmPtB3JMk3n/2WE++ugYQ4fk9zKmRRtkU8KT41Cxnfh9eDqTdsheTynsNMuwVpyEYchL02WCMOofmehJcKw3uaPzi0kNwVGg08ZmgzDk29dyPHVxkh/MrPc36knqfOT8KB88N0w6pu3DCduL8OQQrOTIi4+9nlLYqLxyp76LtdezcRU/CBntSjBTNOndQa9HAzGpITjMNLbNVix3v48CgO36/J9X5vnMpSmmCua668d6EjxxYYz3nBk88EFaeHIINkL8a9hjNsoybJYRCcOQW7ka04U6x/tSmE505yZMrQSCjem0bbMl0+WLz8/w+eemKZrrhdDDY1mevDDO4yd7kA94E6nw5BDcCRGx9piNsgybNZwuVmxu5+vMV2zmKzYn+5Kcn+hCkiRRKhEI1tBp22ZnSyafuTjFn788h7VmJ4wswTvu7ePJC+OcGd79HrfdRJakZllFNI8K7oQQHx3CZn0XVdsjaag8fqKHm7kax3oTq0osDcv2RpOq8O0QHFUqlkux7nbEBMurcxWeemaSb1xdZK0GMlSZH39wiI89OsZod3x/Dtgm4rpCOqYJTw7BthDio0PYrO8iZaiosozlBox2RY2lkiRtOKWyU9+O/RAtQigJ2kGnbJsNwpDv38jz9MVJnp8srbuejWv81PkRfvLcKNnEwW0i1RSZ1LLzqPDkEOwEIT4OANvtE9nORM3K4G+5PtMFkyBkQ9HSbrEgDM4Ed4PtRWOzZottrnuJ4wV89coCT1+c5Fauvu76aFecj18Y470PDGIc0JKEJEkkDYW0oRHXD+ZrEHQOQnzskL28Y99un8h2DMtWBv+lqo0my5wZyWwoWtotFoTBmWAnuH5AoeY0S477RdX2+LMXZvjcc9Pkqs666w8Mp3nisXHefqrvwDZeGlo0rSI8OQTtRIiPHdKuIHw3ImajjMh2fDtWBv9i3cHx/U1FS7vFglhFL9gOfhBSqDtU9nnx20LZ4nPPTvOll2apt8i6vO1UL09eGOeh0e3vWuoEGr+L6Zh24Md9BZ2J+KTfIe0KwncjYjbKiGzHt2Nl8O9N6Yx0xSP3wQ1ES7vFgjA4E2yFIHhj8Vuwj6Lj9YUqT12c5GuvLq6bpNEUib/1wCBPPDrORG9in064cxqeHClDJSGaRwW7jBAfO6RdQXi/yw6tgv9mHzrtFgvC4EywGWEYUrY8SvX9W/wWhiGXbhV46uIUl24V1l1Px1Q+dG6Enzo/Sk9S34cT3h2aIpOJCU8Owd4ixMcO2SwIb6eUsp9lh52UfIRYEOwVVdujUNu/xW+eH/D11xZ56pkpri1W110fzBh8/NEx3vfQ8IFrwJQlieTytIrw5BDsB0J87JDNgvB2Sin7WXY4zJMmYoT34GK5Prmag71Pi9/qjseXXprjc5emWKjY667fM5Dipx8b54fv6z9wmYL4clklZaji90GwrwjxsUW2E8y2U0q5UyZhN4PobpV8OiHwH2ZhdVixPZ9CzaXu7M8ES65q8yfPTfO/XphtOUXz5uPdPPHYOOfHuw5U4G54cqRiKprw5BB0CEdGfGw1IDZW1d/OR7P6Ez2JbRt3tbOUcrdBdLPXvVsln04I/PvdSyPYOp4fkK87VK39ER23cjWevjjFX16ex/VXN5EqssSP3D/AkxfGONmf2pfz7QRJkkguO48etJKQ4GhwZMTHVgPiYsXmW9eWeH2xBsDJviQ/dF//toJZO0spdxtEN3rdYRgShiHZuEoYhiQNtbn1825t2jsh8IsR3s7HD0KKdYfyPozNhmHIi9Mlnnpmku9ez6+7ntAVPvDwMB9909iBmsASnhyCg8KR+UTeakCs2h5V26MrrgESteU/byeYrSyl3G0J4m6D6Eave7Fi89J0GT8IqVgukgQpQ9u2TXur19cJgV+M8HYuYRiNzRbrez826wch37q2xFPPTHJlrrLuem9K56NvGuMDDw8fGMEqPDkEB5GD8dvVBrYaEBvNWPPlNzIfjeC1k2DWKoD3p40tC5K7DaIbve6VouTZWyZIcN/gG86m/WHIrVyN6UKd430pTMfbsuNpOwP/VsTbRt8jpnI6j7LlUqzt/dis5fr8+ctzfObSFLMla931E31Jnrgwxo/cP3Ag+iKEJ4fgoHNkxMdWA2J/2uAd9/Qx0ROZBE30JO4qmLXKPABb7om42yC60eteKUqShooksUqgLFZsbufrzFds5it2U4Rt5fUNZGJtC/xbyb50Qo+JYHP2a/Fbse7whedm+MLz05Rb9JQ8Mt7Fk4+N8ebjPQcigAtPDsFh4ciIj60GcUmSGMzGGcy2Z811q8zDXvRErM0GnOhLrvpwXSlKkssNaTXHbwqUG0s1kobK4yd6uJmrcaw30dLLJFe1qdou08UQVZbbnqreynvVCT0mgtZYbrT4zdrjsdnpgsnTlyb5yg/m1wkeWYIfvq+fJx8b577B9J6eaycITw7BYeTIiI/9YqPMw273RNwpG3AnMZYyVFRZxnIDRrsSHOtdLV4aj+/5AWEIvUmdY73JtvdWbKVc1gk9JoLVOF5Aoe5Q2+PFb6/MlHnq4iTfurrE2m6SmCrzE2eH+dijYwxlOz8zJjw5BIcZ8SndZlr1H6wN8lspAW3W67CVPoi7zQbc6YyNxx/tTizvhTF2pdSxlfdqP5tLO8HTpJPYj7HZIAz5zus5nr44yUvT5XXXuxMaP3V+lA+dGyET1/bsXDtBeHIIjgpCfLSZrfQfbKUEtNnjbOU5tpMN2EnD5lYevx2BeSvv1Va+Z7dEgug3idiPsVnHC/iLV+b5zKWppi/PSsa743z8wjg/9sBgR0+BCE8OwVFEiI8dsFkgu5uMw8rHzVVtvCAqeax9nDs9x0oPD3ijaXYjdhJAt5Jt2I3AvFMRsVsi4aj3m+zHttmK5fLFF2b4k2enKdTdddfPjmZ44sI4bz3Vi9zBWShjeXt02hCeHIKjhxAfO2CzQHY3/QcrH7fhvdHqce70HCs9PBRZQpKkTQP0TgLoVrINuxGYdyoidkskHNV+k8a22WLdWbdafreYK1l89tIU//vlWSx3dROpBLzj3j6evDDOAyOZPTnPTmj8G0nFVAxVZDkER5ej8UnZZjYLZBtlBLbbpzFdCOlN6fSmjHWZhb6UzkhXZALWnzboS+kbPs5WAu1uBdB2PO7a961iuTsSEbv1Gg+zmdlG/2YrVmQQtlfbZl+br/DUM5N8/bVF1uocXZV574ODfPzRMca6E3tynu0iSRLxZedR4ckhEEQI8bGGrYiEzQLZRhmB7fZpqIrMsd5ky7v6parDTNHCD0JmihZ9a5o97xRo177GvpS+KwG0HYF5sWLzwmSRQs3F8X2O9yVR5NYZod0+SysOs5nZ2n+z9w6kUBRpT7w6wjDk4q0CTz0zybO3i+uuZ2IqH35klJ88P0J3Ql//AB2ArsqkDeHJIRC0QogPVgdjy/WZKZr4ARuKhJ0Esq1kI7b6uHd6rFaPs/Y1ThdMgnD1a2x3AG1HYK7aHoWaS8V2WVxeb/6mY93EluvlWxURh1kk7BaNf2d9KYPX5itoisR4z+5mF1w/4GtXFnj64hTXl2rrrg9nY3z80TF+/KGhjvS8UOQ3PDlEWUUg2BghPlh9h7dYsdAUmQdGshuKhJ0Esq2k/bf6uHd6rFaPs1C2mq9xqWqjyTJnRjId3ySZMlQc32exYtOXNtAUmZimHKgNowcVQ5WpWC7zZQtZlkjou/dxUbM9/uzFWT737BRLVWfd9fuH0jz52DjvuKev47IIoqwiEGwfIT5YnUko1V3cIOjo3oC7zbwU6w6O77f1Ne7WKGt/2uBNx7qRJAlVluhN6UemqXO/aHh1WK7Psd4kdccjoav0JNvvkbFYsfncs1N86cVZas56F9S3nOzhycfGeXg023FBXZRVBIKdIz7FWZ1J6E5qjHTFqNkexbrLzaUqYRgykInd1YdfO9P+d5t56U3pjHTFm6WLvpTOfMlseiVM9CS2/Xp3a5RVkiTODGfoSxkblpGEuVd7WOvVIUmR2Oul/T0VN5ZqPH1xkq9eXsBb00WqKRI/emaQj18Y43hvsu3PfTeIsopA0B6E+GB9JiEMQ67MVXh9sbHZ1uSH7uvfcjDtxMDYKlvSONNC2eKbV5eaNfZT/UneeW//trbv7qbfxZ3KSEfZ3KsdBEEYbZvd5RX3YRjy3GSRp5+Z5Ps3C+uuJw2FD50b4SPnR+lNdc7UkNggKxC0HyE+WB/cri9WqdoeXXENkKjZrdfJb0Qnul5uli2p2h4126MrrgMh1eXXC1vfvrvXfhdH3dyrHTS8Okr13V1x7wchX39tkaeemeTqQnXd9YG0wUcfHeP9Z4d2ta9ku4iyikCwe3TOb/ous51sRGOZ03y5kflovU5+I+42MLYrc7LR46z9elJXSBoq85XlzEcque3tu3vtd3FUzb3aRTRF5OyqV4fp+Hz55Vk+e2maubK17vo9/SmefGyMH76vH7VD9pgIE7CDTydmngXrOTKf2BtlI4Ig4MpcpWnYdf9Qmv60wTvu6WNieazwTvbka7nbwNiuzMlGj7P669H44kRPnExMpSuhrdpOu9XXsdejrIfZ3Gs3qTse+Zqzq14d+ZrD55+b5osvzFBpsWDuwrFunnxsnDdNdHVEUBBllcNFJ2aeBes5MuKjYrnkqw6ZuEq+6lKxXAYyMa7MVfjyS3O4ftDcIvnASJbBbJzBbHzLj79SbSd1hbOjGWqOv6PA2K6SwkaPs/Lrr8yUmCtZ9KdjKLLM8b5U8xe1kwO88O3YHpbrU6g7mC0mStrF7Vydz1ya4v+8Mofrr+4dUWSJd5/u58kL45wa6IwxaV2VSce05s2C4HAgSrIHgyMjPmwvYLJQx12KRMZDY9H+h8WKjesHnB7K8OpcuWlktV1aqe2VXhTbLfu0o6Sw0eOs/LoXhOiK0vIXtVMDfCemVTvxTBBtfi3UHWr27qy4D8OQl6fLPHVxkm+/nlt3Pa4pfODhYT76ptGOuPsUZZXDjyjJHgyOzN+KocqMdcfJJjRKdRdjecV2/7Jx1atzZTRF3vHd/Z3U9nZSge3IOGy22Xbl44/3xJkumHvyi9quAN14L70goGZ7TPQkmqWi/Qr4nZbqbXh1VFuUPdqBH4T8zetLPP3MJK/MVtZd703q/NT5UT50boRUbH8/ZhpllXRMJa6Jssphp5MztoI3ODLiIx3T6E0Z+EFIb8ogHYsMk+4fSgOs6vnYCXdS29tJBbYj47DZZtuVjx+G4ToPjd2iXQG68V7GNYUXp0pULY+S6e1rwO+UVO9ar452Y7s+X3llns9emmKqYK67fqw3wRMXxnnP/QPo6v42kYqyytGkUzO2gtUcGfGxkRqW5chKfbcev8Fm4uROGYEwDFkoW9syAdtKMGy1YG43SwftCtCN9/JmLprOOd6XwnL9fa3t7lWqd7MJppK5e14dpbrLn74wzReem6FouuuunxvL8uRj47z5RA/yPmYWRFlFIDgYtP0T0vd9/tW/+lf8t//235ibm2NkZIS/+3f/Lr/+679+qNOdd1Lbm4mTO2UEFis237q2tML0LNnS9Gzt8jhZ2nz769rnHemKNbfl7kbpoF0BuvFeZuMqSb2O6Xioiryvtd29SvWu/Ts7O5ohpqu75tUxUzT5zKUp/vzlOew1EzKyBO+8t58nHxvj/qFM2597q4iyikBw8Gj7p/Xv/u7v8vu///v88R//MQ8++CAXL17k53/+58lms/zSL/1Su59uy2wU4BsBu2K52F6AsZyqbfdd/51MvhoZgelCnVu52qog1jD9upPp2doR2tHu+KbbX9dmIhYr9q6WDtoVoBvvZX/a4FhvsiNqu3uV6l35d3Z9scq1xSrD25jK2ipX5so89cwU37y6yBr3cwxV5scfGuLjj44x0tX+594qxvK/bVFWEQgOHm0XH9/+9rf5yZ/8Sd7//vcDcPz4cf7n//yffP/732/3U22LjVL+jYCdq9pMFUzGuxP0pPQ97R9YmRGo2h41xyNfc5siaSumZ2EYcitXY7pY53hvEtP1N9z+2hBcuapN1XaZLoaoctRsO1O0dq100O4Avdnjder0yd2SMlQ8P+ClqSIBtDX4B2HI967neeriJC9OldZd74przSbSbKL9S+a2girLJA2FdEzb954SgUCwc9ouPt72trfxB3/wB7z22mvcd999vPDCC3zrW9/i937v91p+v23b2PYb463lcrndRwI2Tvk3REk2oXFjqUYmruIH4Z72D6zMCOSqNrmas0oknehLNk3PwjAkaahULLf5s5IksVixuZWrM1+2mS/bnOrf2JW1OS3iB4RhNJlwrDdJX0rfs+bT3WY3p0/2S9hYro8XBAxkYqRiats2zTpewFcvz/P0pSlu5errro91x/n4o2P82AODGNre91FIkkRSV5qvWSAQHHza/pv8a7/2a5TLZe6//34URcH3fX77t3+bn/mZn2n5/Z/61Kf4rd/6rXYfYx19KZ2RrlhzqqUvFW3qbIiSXNVBlSWm8iYxXSJpKIRhuGEJpp0BaOUdfMpQKZneKpEkSVLT9GyjhWqNczx+opebS9VNXVkbgmu0O7G85dZoBuatZiY2e/07fW/a+Z7u5vTJXo/VrvXqaNem2arl8cUXZvj8c9Pkas666w8MZ3jysXHedqp3X8oaoqwiEBxe2i4+nn76af77f//v/I//8T948MEHef755/nlX/5lRkZG+Lmf+7l13//JT36ST3ziE80/l8tlxsfH230sFis2l2fLVG2PpapNb1JnMBtvZh0qlstod5ybSzVML+C713NMdCc3LMHsVgBa2xfRl9JZKFvNP1cst2VQTRkqqiJjuT6j3ZHvxVZNzJK6suo5thL0N3v9d+qv2eh52vme7ub0yV6N1Xp+QKHuNrNc7WK+bPG5Z6f40otzmO56x9O3n+rlycfGeWj07qfAtosqy6RikeAQZRWB4PDSdvHxK7/yK/zar/0aP/3TPw3A2bNnuXXrFp/61Kdaig/DMDCM3U/v387XeX2xRldcY75cY6Insco+XZIkDFWmL20QhiG3l2qYKZdcNWxasa9kJ6OsWwnqa/sY1mY6RrpiLYPqdpo5135vEAR88+oSNdsjaai8896+O1rLb/b679Rfs5G4aGdQ383pk90eq/WDaGy2ZLpt9eq4tlDl6YuT/NWVhXVNpJoi8WMPDPHxC2PNnUZbJQxD8jWXuuM1S0HbyViJsopAcPRo+296vV5H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ueOOO3j00Ue7Hdaa+exnP4umaXz4wx/udigd8clPfhJN01Z87dixo9thddSRI0f4gz/4A8rlMq7rcsMNN/D00093O6yO2bRp0yl/h5qmsXv37m6HdtGSJOEv//Iv2bx5M67rsnXrVv7qr/7qvM5fWUvrIvn4l3/5Fz7ykY/wwAMP8Oyzz3LjjTfyG7/xG0xPT3c7tI7wPI8bb7yRL33pS90OZU08/vjj7N69m6eeeorHHnuMKIp4z3veg+d53Q6tI8bGxvjsZz/LM888w9NPP8073/lOfuu3fotf/epX3Q6t4/bs2cNXvvIVdu3a1e1QOur666/n2LFjy19PPvlkt0PqmIWFBd7ylrdgWRaPPvooL7zwAn/7t39Lb29vt0PrmD179qz4+3vssccAeN/73tflyC7e5z73Ob785S/zxS9+kRdffJHPfe5z/M3f/A1f+MIXuhuYWgduu+02tXv37uXvkyRRo6Oj6sEHH+xiVGsDUI888ki3w1hT09PTClCPP/54t0NZM729veof//Efux1GR9XrdXXttdeqxx57TL3jHe9Q999/f7dD6ogHHnhA3Xjjjd0OY8189KMfVW9961u7HcYldf/996utW7eqNE27HcpFu/vuu9V999234rHf+Z3fUffcc0+XImq76mc+wjDkmWee4d3vfvfyY7qu8+53v5v//d//7WJk4kJVq1UA+vr6uhxJ5yVJwje/+U08z+OOO+7odjgdtXv3bu6+++4V/y9eLV599VVGR0fZsmUL99xzDwcPHux2SB3z7//+79xyyy28733vY3BwkJtuuomvfe1r3Q5rzYRhyD//8z9z3333dfxw02648847+eEPf8grr7wCwC9+8QuefPJJ7rrrrq7GddkdLNdps7OzJEnC0NDQiseHhoZ46aWXuhSVuFBpmvLhD3+Yt7zlLezcubPb4XTMc889xx133IHv++TzeR555BHe9KY3dTusjvnmN7/Js88+y549e7odSsfdfvvtfP3rX2f79u0cO3aMT33qU7ztbW/j+eefp1AodDu8i7Z//36+/OUv85GPfIRPfOIT7Nmzhz/5kz/Btm3uvffebofXcd/5zneoVCp84AMf6HYoHfGxj32MWq3Gjh07MAyDJEn49Kc/zT333NPVuK765ENcXXbv3s3zzz9/Va2pA2zfvp29e/dSrVb513/9V+69914ef/zxqyIBOXToEPfffz+PPfYYmUym2+F03Il3kLt27eL2229nYmKCb33rW/zhH/5hFyPrjDRNueWWW/jMZz4DwE033cTzzz/PP/zDP1yVycc//dM/cddddzE6OtrtUDriW9/6Ft/4xjd4+OGHuf7669m7dy8f/vCHGR0d7erf31WffPT392MYBlNTUysen5qaYnh4uEtRiQvxoQ99iO9973s88cQTjI2NdTucjrJtm2uuuQaAN7/5zezZs4e///u/5ytf+UqXI7t4zzzzDNPT09x8883LjyVJwhNPPMEXv/hFgiDAMIwuRthZPT09bNu2jddee63boXTEyMjIKUnwddddx7/92791KaK1c+DAAX7wgx/w7W9/u9uhdMyf//mf87GPfYzf+73fA+CGG27gwIEDPPjgg11NPq76mg/btnnzm9/MD3/4w+XH0jTlhz/84VW3pn61UkrxoQ99iEceeYT//u//ZvPmzd0Oac2laUoQBN0OoyPe9a538dxzz7F3797lr1tuuYV77rmHvXv3XlWJB0Cj0WDfvn2MjIx0O5SOeMtb3nLK1vZXXnmFiYmJLkW0dh566CEGBwe5++67ux1KxzSbTXR95VBvGAZpmnYporarfuYD4CMf+Qj33nsvt9xyC7fddhuf//zn8TyPD37wg90OrSMajcaKu6zXX3+dvXv30tfXx/j4eBcj64zdu3fz8MMP893vfpdCocDk5CQApVIJ13W7HN3F+/jHP85dd93F+Pg49Xqdhx9+mB//+Md8//vf73ZoHVEoFE6pz8nlcpTL5auibufP/uzPeO9738vExARHjx7lgQcewDAMfv/3f7/boXXEn/7pn3LnnXfymc98ht/93d/lZz/7GV/96lf56le/2u3QOipNUx566CHuvfdeTPPqGRrf+9738ulPf5rx8XGuv/56fv7zn/N3f/d33Hfffd0NrKt7bS6hL3zhC2p8fFzZtq1uu+029dRTT3U7pI750Y9+pIBTvu69995uh9YRp3tvgHrooYe6HVpH3HfffWpiYkLZtq0GBgbUu971LvVf//Vf3Q5rTV1NW23f//73q5GREWXbttqwYYN6//vfr1577bVuh9VR//Ef/6F27typHMdRO3bsUF/96le7HVLHff/731eAevnll7sdSkfVajV1//33q/HxcZXJZNSWLVvUX/zFX6ggCLoal6ZUl9ucCSGEEGJdueprPoQQQghxeZHkQwghhBCXlCQfQgghhLikJPkQQgghxCUlyYcQQgghLilJPoQQQghxSUnyIYQQQohLSpIPIYQQQlxSknwIIYQQ4pKS5EMIIYQQl5QkH0IIIYS4pCT5EEIIIcQl9f8DvBq4eqmKlScAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } + ], + "source": [ + "import seaborn as sns\n", + "\n", + "# draw the graph. This might take ~30 seconds.\n", + "sns.regplot(x=\"new_cases_percent_of_pop\", y=\"search_trends_cough\", data=weekly_data, scatter_kws={'alpha': 0.2, \"s\" :5})" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "id": "5nVy61rEGaM4" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGeCAYAAAA0WWMxAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAArzVJREFUeJzs/XmMnOl1349+3r32qt437rORM+SMJMvWYkmWYtkaymtyE8O5RqDYQBLAAWxHQGwrsA07sK04fxhGcgM7zgWcBNkQ3Fz7l5ufZrxKtmRLsuSRNMMZkjMckk2y96qu/d3f97l/vFU13c3qjey9ng9Ay+yu7nq6OF3n+5zzPecoQgiBRCKRSCQSyQGhHvYBJBKJRCKRDBZSfEgkEolEIjlQpPiQSCQSiURyoEjxIZFIJBKJ5ECR4kMikUgkEsmBIsWHRCKRSCSSA0WKD4lEIpFIJAeKFB8SiUQikUgOFCk+JBKJRCKRHCj6YR9gI3EcMz8/Tz6fR1GUwz6ORCKRSCSSHSCEoNlsMj09japuk9sQu+TP//zPxfd///eLqakpAYjf//3f733O933xsz/7s+Ly5csik8mIqakp8Q/+wT8Qc3NzO/7+9+/fF4D8I//IP/KP/CP/yD/H8M/9+/e3jfW7zny0221eeOEFfuInfoK/83f+zrrP2bbNK6+8wi/+4i/ywgsvUK1W+emf/ml+8Ad/kK9//es7+v75fB6A+/fvUygUdns8iUQikUgkh0Cj0eD06dO9OL4VyuMsllMUhd///d/nh3/4hzd9zNe+9jW+4zu+g9nZWc6cObPt92w0GhSLRer1uhQfEolEIpEcE3YTv/fd81Gv11EUhVKp1PfznufheV7v741GY7+PJJFIJBKJ5BDZ124X13X5uZ/7Of7+3//7m6qgz372sxSLxd6f06dP7+eRJBKJRCKRHDL7Jj6CIOBHfuRHEELw27/925s+7jOf+Qz1er335/79+/t1JIlEIpFIJEeAfSm7dIXH7Owsf/Znf7Zl7ceyLCzL2o9jSCQSiUQiOYLsufjoCo+33nqLz3/+84yMjOz1U0gkEolEIjnG7Fp8tFotbt261fv7nTt3+OY3v8nw8DBTU1P83b/7d3nllVf4P//n/xBFEYuLiwAMDw9jmubenVwikUgkEsmxZNettl/4whf42Mc+9tDHP/WpT/HLv/zLnD9/vu/Xff7zn+ejH/3ott9fttpKJBKJRHL82NdW249+9KNspVceY2yIRCKRSCSSAUAulpNIJBKJRHKgSPEhkUgkEonkQJHiQyKRSCQSyYGy7+PVJZKtEEKw0vRoeSE5S2csb6EoymEfSyKRSCT7iBQfkkNlpenx6oM6USzQVIXnTxUZL6QO+1gSiUQi2Udk2UVyqLS8kCgWTJfSRLGg5YWHfSSJRCKR7DNSfEgOlZylo6kK8zUHTVXIWTIZJ5FIJCcd+U4vOVTG8hbPnyqu83xIJBKJ5GQjxYfkUFEUhfFCivHDPohEIpFIDgxZdpFIJBKJRHKgSPEhkUgkEonkQJHiQyKRSCQSyYEixYdEIpFIJJIDRYoPiUQikUgkB4oUHxKJRCKRSA4UKT4kEolEIpEcKHLOh2RfkAvjJBKJRLIZUnxI9gW5ME4ikUgkmyHLLpJ9QS6Mk0gkEslmSPEh2RfkwjiJRCKRbIaMCJJ9QS6Mk0gkEslmSPEh2RfkwjiJRCKRbIYsu0gkEolEIjlQpPiQSCQSiURyoEjxIZFIJBKJ5ECRno8TiBzwJZFIJJKjjBQfJxA54EsikUgkRxlZdjmByAFfEolEIjnKSPFxApEDviQSiURylJFR6QQiB3xJJBKJ5CgjxccJRA74kkgkEslRRpZdJBKJRCKRHCgy8yGR7AGyvVkikUh2jhQfEskeINubJRKJZOfIsovkSCOEYLnhcnulxXLDRQhx2Efqi2xvlkgkkp0jMx+SI81xySjI9maJRCLZOfIdUnKkWZtRmK85tLzwSHbxyPZmiUQi2TlSfEiONMcloyDbmyUSiWTnHM13comkg8woSCQSyclDig/JkUZmFCQSieTkIbtdJBKJRCKRHChSfEgkEolEIjlQBr7sIidTSiQSiURysAy8+DgucyQkEolEIjkpDHzZRU6mlEgkEonkYBl48XFc5khIJBKJRHJSGPhIK+dISCQSiURysAy8+JBzJCQSiUQiOVgGvuwikUgkEonkYJHiQyKRSCQSyYEixYdEIpFIJJIDRYoPiUQikUgkB4oUHxKJRCKRSA4UKT4kEolEIpEcKFJ8SCQSiUQiOVCk+JBIJBKJRHKgSPEhkUgkEonkQNm1+PiLv/gLfuAHfoDp6WkUReEP/uAP1n1eCMEv/dIvMTU1RTqd5uMf/zhvvfXWXp332CKEYLnhcnulxXLDRQhx2EeSSCQSieRQ2LX4aLfbvPDCC/y7f/fv+n7+X//rf82/+Tf/ht/5nd/hq1/9Ktlslk984hO4rvvYhz3OrDQ9Xn1Q562lFq8+qLPS9A77SBKJRCKRHAq73u1y9epVrl692vdzQgh+67d+i1/4hV/gh37ohwD4z//5PzMxMcEf/MEf8KM/+qOPd9pjTMsLiWLBdCnNfM2h5YVyn4xEIpFIBpI99XzcuXOHxcVFPv7xj/c+ViwWed/73seXv/zlvl/jeR6NRmPdn5OGEAI3iCi3PN6Yr6OpkLMGfqefRCKRSAaUPRUfi4uLAExMTKz7+MTERO9zG/nsZz9LsVjs/Tl9+vReHulIsNL0mKs6GKpKEMVMl9KM5a3DPtaJRnpsJBKJ5Ohy6N0un/nMZ6jX670/9+/fP+wj7TktLyQWcGm6wFg+RcrQUBTlsI91LHhUESE9NhKJRHJ02VPxMTk5CcDS0tK6jy8tLfU+txHLsigUCuv+nDRylo6mKszXHDRVkSWXXfCoImKtxyaKBS0v3OeTSiQSiWSn7Kn4OH/+PJOTk/zpn/5p72ONRoOvfvWrfOADH9jLpzpWjOUtnj9V5KmJHM+fKsqSyy54VBEhBZ9EIpEcXXb9jtxqtbh161bv73fu3OGb3/wmw8PDnDlzhp/5mZ/hV3/1V3nqqac4f/48v/iLv8j09DQ//MM/vJfnPlYoisJ4ISW7Wx6BRxURXcHX8kJyli4Fn0QikRwhdi0+vv71r/Oxj32s9/dPf/rTAHzqU5/iP/7H/8jP/uzP0m63+cf/+B9Tq9X40Ic+xMsvv0wqldq7U0sGhkcVEVLwSSQSydFFEUesDaDRaFAsFqnX6yfS/yGRSCQSyWEgRFK6doKI8fzeJwR2E79lIVwikUgkkhNMHAsabkDDCQnjGFM/9EZXKT4kEolEIjmJhFFM3QlouiHx0SpySPEhkUgkEslJwgsj6k5A24uO7IBFKT5I6mArTW+dqVEOAZNIJBLJccLxI2qOj+NHh32UbZHig3cGWUWxQFMVnj9VZLwgu3MGHSlKJRLJUadrIq07AX4YH/ZxdowUH8iNs5L+SFEqkUiOKnEsaLqJ6Ajj4yM6ukjxgZyGKemPFKUSieSoEUYxDTek4QRHzkS6G2SURU7DlPRHilKJRHJU8MOYmuMfaRPpbpDvpshpmJL+SFEqkUgOG8dPOlds/2Qtx5TiQyLZBClKJRLJYdE1kXrB0e9ceRSk+JBIJBKJ5AgQx4Kml/g5guj4mUh3gxQfEolEIpEcIlEsOpNIA6L4+Ps5doIUHxKJRCKRHAJ+mIw/b3nhiTCR7gYpPiQSiUQiOUDcoDv+/GSZSHeDFB+PwHGffHkUzn8UziCRSCQHSdsLqZ1gE+lukOLjETjuky+PwvmPwhkkEolkvxFC9IaCnXQT6W5QD/sAh40QguWGy+2VFssNd0d1t7WTL6M4mat/nDgK5z8KZ5BIJJL9IooF1bbPvVWbSsuTwmMDA5/5WGl6fOt+jWo7wI8i3nN2iEtThS1LAEdh8uXjlC2Owvl3cgZZmpFIJMeNIIo7nSuDZyLdDQMvPlpeSLUd0PQCVpoeiqIwmrO2LAEchcmXj1O2OArn38kZZGlGIpEcF6SJdHcMvPjIWTp+FLHS9BjNW+iqsu0CsaMw+fJxlp4dhfPv5AxysZtEIjnqtDuTSF1pIt0VAy8+xvIW7zk7hKIo6KrCSM48FgvEsqZGywt45Z5DztLJmtphH2nPOQrlIYlEItmIEMkk0rotTaSPysC/myuKwqWpAqM569gtEBMCEJ3/PYEchfKQRCKRdIliQdMNqDuDM4l0vxh48QFHowyxW9p+RD5l8MxkgfmaQ9s/eSm/4/jvIpFITh5dE2nLDYlP6m3vgJHi45giSxISiUSyv7hBRKMz/lyyt8iIdUyRJQmJRCLZH2w/MZE6JzCjfFSQ4uOYIksSEolEsncIkQw7rEkT6YEgxYdEIpFIBpY4FjTcgIYTEsaDIzqEEIc6tFGKD4lEIpEMHOGaSaSDZCK9U27zR68v8vpCg//rn34ITT0cASLFh0QikUgGBi/sTiKNBmb8ecsL+fyNZV66tsiNxWbv4198a4WPPnM4xXspPiQSiURy4nH8iJrjD4yJVAjBqw/qfO7aIn/x5gpe+HBJ6X+9MifFh2TvOazFbHIhnEQiOQp0TaR1J8DvE3xPIitNjz98fZGXX19kvub2fczFyTw/9v6z/OAL0wd8uncYePHRL1ACJyJ4HtZiNrkQTiKRHCZxLGi6iegYBBOpH8Z8+XaFl64t8vW7q/QbvlpI6Xz82QmuXp7k0lSBU0OZgz/oGgZefPQLlMCxCZ5bZRkOazGbXAgnkUgOgzCKabghDScYCBPp7ZUWL11b5I/fWKLhPjwITQG+/dwQL16e4oNPjGDq6sEfchMGXnz0C5TAsQmeW2UZDmsK6mFOX5UlH4lk8BgkE2nLDfmzm4l59OYa8+hapoopXrw8ySeenTiyF+eBFx+bBcrjMrp8qyzDYU1B3fi8ozmT5YZ7IIJAlnwkksHB8RPRYfsne/x5LATful/jpWuL/MVb5b7+FVNX+chTo1y9PMkLp0uoR/zSdXSj6gGxWYDu97GjeKveKstwWFNQNz7vcsPtCQJVgZmhNClD25fXUJZ8JJKTT9dE6gUnu3Nlpenx8uuLvHxtkYV6f/PoMxN5Xrw8yXdfHCeXOj4h/ficdJ/YLED3+9hh3aq3Ej3HYcfLWkFwfb7BctNjNGfty2soF+5JJCeTOBY0vcTPcZLHn/fMo68t8PXZ6qbm0e95doIXL0/yxFju4A+5B8h35g1sZ+AMo5i0qXO33KKYPpjsx1ai5zCyG7vNAK0VBH4UYWjqvmUmjoMYk0gkO6drIm26AVG/SHxCuL3S4nPXFvmTY2gefRSk+NjAdgbOlhfy6lw9+fuqzdmR7L5nP45aKWG3GaC1guD0cPIz7FdmQi7ck0hOBn6YjD9veeGJNZH2zKOvLXJzaXPz6NXLk3zvETaPPgpSfGxgOwPn2ZEMbT/k3EgWJ4gORAgcVClhpxmN3YqhtYJACMFozpKZCYlE0hc3iKjZJ9dEuhPzqKWrfOTpMV58buJYmEcfBSk+NrCdgfPsSJa6E+IGMbqqHoinYK9LCZuJjJ1mNB5HDMnMhEQi6UfbC6mdYBPpcsPlD19f4uXXNzePXpxMzKN/6+L4iferneyfbgdsDMSjOXPLQH8YnoK9DtibiYydZjSkr0IikewFQojeULCTaCL1w5i/ervcmTxapV/xqGsevXp5kgvH1Dz6KAy8+NgsEG8W6E/Czb3pBlRaHsWMQaXl03QDxgupHWc0TsJrIJFIDo8oFjScgMYJNZG+vdLipdcW+ZPr/c2jqgLvPTfMJy9P8oEnRjC0420efRQGXnwcNTPnQeCFMQ+qDnfKbQxN5UpnpLzMaEgkkv0kiGJq9sk0kbbckD+9scxL1xZ4c6nV9zHTpa55dHLg318HXnx0b/tzNZu2F1JpeUdmgNh+Yekqp4cyFNI6DSfE6rRsyYyGRCLZD9ygO/78ZJlIYyH45v0aL+/APPrJy5NcOVU8kebRR2HgxUf3tj9badNyQyotn7oTcmWmgKIoR2qa6V6RTxkM50yiWDCcM8mnjMM+kkQiOYG0O5NI3RNmIl1quPzRDsyjn7wyyUefOfnm0Udh4F+R7m2/5YWstoNe+eXeqk3dCbft/NjNwK2jMp5dllckEsl+IUQyibRunywT6U7Mo8W0wfd2Jo+eH80e+BmPEwMvPrpsNFvCzjbb7mbg1lFZenbSyitHRdRJJIPMSTWRvr2cTB790y3Mo99xfpgXL0/ygQuDaR59FKT46LAxGyCEoO40tu382I1hdRDNrQfBURF1EskgEkTJJNKme3JMpE034M9uLPO51xZ5a7m/eXSmlObq5Um+59kJmT1+BAZWfPS7LY8XUoyt+fh0KYWlq+RTxqb/ce1m4JZcerY/SFEnkRw8bhDR6Iw/PwnEQvDNezU+d22RL761QhA9LKRSHfPo1SuTPD9TlBnWx2Bgo99mt+XH2Vuy1j/RT9xIr8X+IEWdRHJw2H5IzT45JtLFhssfXlvk5dcXWWp4fR9zaSrP1ctTfOyZMbLy/WVPGNhXsXtbniqmuLHQ5PpCA6C3OXG7W/RGcXF+NLtOBW86vOwEeS2OClLUSST7ixCClpeIjpNgIvXDmL+8VeZz1xZ5Zba/ebSUNvj4s+NcvTwlzaP7wMCKj+5t+cZCk/tVG4EgiATTpdSObtHbZUhkKeDgOGkGWonkqBDHgoYb0HBCwvj4i463lpq8dG2RP72xTFOaRw+VgRUf3dvy9YUGAsGl6QILNRdLV3d0i95OXOxVKUB2ckgkkoMmXGMijY+5ibThBJ3Jo4vc2sY8+r3PTTCak5nTg2BgxUf3tgwQRIKFmoumKuRTxo5u0duJi70qBchODolEclB4YUTdDmj70bHuXImF4JXZKi9dW+RLt8qbmke/65kxXrwszaOHwcCKjy79RMJOsg3biYu9KgXI8o1EItlvbD+ZROr4x9tEuthwefnaIi9fW2S52d88+mzHPPpRaR49VAb+le8nEpYb7rbZhoPyGchODolEsh90TaR1J+i7k+S44IcxX3yrzMvXFnjlXm1T8+j3PDvB1SuTnBuR5tGjwJ5HsiiK+OVf/mX+y3/5LywuLjI9Pc0//If/kF/4hV84Nmmto5RtOKqdHNKLIpEcT+JY0HQT0XGcTaRvLTU7k0eX+84aURV43/kRrl6e5P0XhtGlebTHUXiv3nPx8Ru/8Rv89m//Nv/pP/0nnnvuOb7+9a/z4z/+4xSLRX7qp35qr59uz1gbTN0gQlXYVbZhv4LxUe3kkF4UieR4cRJMpA0n4E+uL/PytUVurfQ3j54aSvPic9I8uhFVUchYGhlTJ2Noh32cvRcff/VXf8UP/dAP8X3f930AnDt3jv/+3/87f/3Xf73XT7WnrA+mMDOUJmVoO842DFowPkrZIYlEsjle2F1nfzxNpLsxj169PMkVaR7toasqGUsja+qkDPVIvS57Lj4++MEP8ru/+7u8+eabPP3003zrW9/iS1/6Er/5m7/Z9/Ge5+F57xiDGo3GXh9pR2wMpilD48JY7pG//qQH48SLAm/M1wljwenhNEKII/Uft0QyyDh+Ijps/3iOP1+su7z8+tbm0eemC1y9PMlHnxkjY0o/HIChqWQtnYypkToCGY7N2PN/rZ//+Z+n0Whw8eJFNE0jiiJ+7dd+jR/7sR/r+/jPfvaz/Mqv/MpeH2PXPK6xc9CMoWN5i+lSmsW6i6lpzFUdRnPWic72SCRHHSEEbT+iZvvH0kS6E/PoUOadtfVnpXkUgJSRZDcylnZsBqPteYT8n//zf/Jf/+t/5b/9t//Gc889xze/+U1+5md+hunpaT71qU899PjPfOYzfPrTn+79vdFocPr06b0+1pYIIRBCUEwnL8eZ4cyWO1r63e6PqjF0v1AUhZShMZZPPVa2Z+PrO5ozKbd8aWSVSHZB10TacI/f+HMhBG8tt5LJo9uYRz95ZZL3nZfmUUVRSBtar6SiqcfvPXLPxcc//+f/nJ//+Z/nR3/0RwG4cuUKs7OzfPazn+0rPizLwrION1CvND1em2v0/BqKovQC3kn3cjyOUXYvsj0bX9/pUor5mnskX2/Z4SM5aoRRTMMNezupjhN1J+BPry/z0rUF3l5p933MqaHO5NFnJxgZcPOopiqkzURspA0N9RgKjrXsufiwbRtVXa9KNU0jPqItXUIIZitt5mo250ayOEG07ga/Uy/HcRUpj3Puvcj2bHx9V5rekfXOHNd/Y8nJww+TzpWWFx4rE2kUC165V+Wl1xb5y7c3MY8aKh99epxPXpnkuenCQAv8o2wYfVz2XHz8wA/8AL/2a7/GmTNneO655/jGN77Bb/7mb/ITP/ETe/1Ue8JK02O2YrPU8FhqeDwxll13g9/p7f64Gk4f59x70Qa88fUdy1vM19wj6Z05rv/GkpODG0TU7ONnIl2oO/zhtSVefn1r8+gnL0/yXQNuHj0uhtHHZc//hf/tv/23/OIv/iI/+ZM/yfLyMtPT0/yTf/JP+KVf+qW9fqo9oXtrf9/5Ee6WW+v8HrDz2/1uShBHKX2/2bkP6owbX9/RnMlozjqS3plBMxVLjg7dSaRecHzGn3tBxJc6a+u/ca/W9zFd8+jVy1OcGckc7AGPEJahkTWTGRymPhh+FkUcsZxdo9GgWCxSr9cpFAr7/nw7GaW+E3YTrPfqOfeCzc692RmPknA6aAb5Z5ccPEIIGm5Iwzk+JtKeefS1ZG39ZubR919IJo8Osnk03REbWVM7Ma/BbuL3wF/d9qpLZTcliL1K3+9FMNzs3JudcZB9D0d12qzkZBHFgoYT0DhGJtLEPLrES9cWNzWPnu6aR5+bZDhrHvAJD59uh0q2M2X0OHao7CUDIz42C9SHEVD2Kn2/n0JgszPut+9BZhckg8pxM5F2zaOfe22Rv9rCPPqxZ8a5enkwzaOqopAxNTJWMtL8uHeo7CUDIz6O0o19r7It+ykENjvjfvsejtK/k0RyELhBd/z58TCRztccXn59kT+8tsRKq7959PJ0gatXpvjo02OkzZNrmuyHrqqkzWQ1x0nrUNlLBkZ8NN2A1ZZPIa2z2gpousGhBbW9yrbspxDY7Iz7PUxNdpRIBoV2x0TqHgMTqRdEfPFWmc+9tsg379f6PmYoY/CJ5yZ58fIkZ4YHyzxqaCoZUyNr6Se6Q2UvGRjx4YUx96s2QTnG0FQun0rMMMc5zX8YU1X3u0y1naDa6t/rOP9bSgYDIQRNL6RuH30TqRCCN5dafO7aAn92Y5m297BIUhX4wIURXhxA86ipq72R5pYuBcduGRjxYekqp4bSFDMGdTvA6rQzHec0/0k0QG4nqLb69zrO/5aSk81xMpHW7YA/ubHES68tcrvc3zx6ZjjDi53Jo4NkHj2OO1SOKgMjPvIpg5GcRRQLRnIW+ZQBrE/zz1VtZittmm6AF8ZYuko+ZezJDXq7W3m/zwNH/ibfPfdevWbbCaqtyjKyZCM5agRRYiJtukfbRBrFgr+ZrfK5awv81a0KYR+BlDY0PnYxWVv/7NRgmEdPwg6Vo8rAiI+dGChbXkjbD7m90uZB1eH0UIbhnLknN+jtbuX9Pg8c+Zt899yVlrfmNTOYLqVJGdqei6atyjJyCJjkqOAGEY1O58pRZifm0SszBV68PDjmUdmhcjAMzLvzTgyUlZZHpe0jhKA+5zOaNVhtsSfm1O1u5f0+DxzKTX433onuuYsZgzvlNoW0TqXls1h3Gcun9lw0bVWWGbTNwpKjh+2H1OyjbSJ1g4gvvlXmpWubm0eHs2Zvbf0gmEc1VUkGflkaaUMbiKzOYTMw4mMz1oqSnKVTd0LuVNqs2j5iBUoZs2dO3Q0bA3jW1La8la+/tSdvEG0vpOUFzNUEuqoe2E1+N96J7rkrLR9DU2k4IWEsMDVtX0TTVmWZk+iBkRx9joOJVAjBzaUmL11b5M+uL9P2HxZHmqrw/gvDncmjIye+xCA7VA6XgRcfa+nenHUViAWnhtM0nLBnTt0NGwP4lZnClrfytbd2N4iYqzpEsUAIGMmanB3JHthNfjfeie65m27AlVNFLF3FC2Pmqo4sfzwCsmPn+BDHgoYbdAT30RQddTvgj68v8fK1rc2jVy9P8j0DYB6VHSpHBxkV1tC9OQMEkaDaTm4yXhgjhNhVENgYwNt+xIWx3KZBfO2t/fZKi1jAzFCG+ZrDSM7a930za9mNd6J37jXnE0Ic2eVwRx3ZsXP0CdeYSOMjaCKNYsHXZ1d56dri1ubRZ8a4euXkm0dlh8rRRIoPHg7SozmTmaE0y00PQ1OZrzmM7lIAPI75cS/Hr3/rfo1qO8CPIt5zdohLO3ijeVzvxH6XP05ydkB27BxdvDCibge0/ehIdq7M1RxevrbIH76+SLnl933MlZkiVztr69MntNQgO1SOB1J80P+2mTI0RnPWuiAwtoug9zgBfC/Hr1fbAU0vYKXpoSjKjkTUUfdOnOTsgOzYOXrYfjKJ1Onjkzhs3CDiL94q8/K1Bb55v973MSNZk+95doKrlyc5fULNo7JD5fgh39nof9vsFwR2E/QeJ4Dv5fh1P4pYaXqM5i10VTkRN+mTnB2QHTtHAyEErc74cz88Wn4OIQQ3FhPz6OdvbG4e/cCFET55ZZJvPzd8Im//skPleCPFB93bJrw+V6PqBCiK4PmZIldmCrT9qBcE7pTbxyrojeUt3nN2CEVR0FWFkZx5Im7SJzk7cNSzTiedOBY03UR0HDUTac32+ePry7z02gJ3K3bfx5wdznD1SmIeHcqcPPOo7FA5OZycd+3HYCyflFfeWmqy1PBpdsxkH35qjAtjud7j9iroHZRnQVEULk0VtjV/HjcPhcwOSPaao2oijWLB1+4m5tEvv93fPJoxNT76zBifvDzFpan8kf7dfRRkh8rJRIoPkiCdMjQyps50SQOSlOvGzMZeBb2D9Czs5CZ93DwUMjsg2Su8sLvO/miZSLvm0ZdfX6SyiXn0+VOJefQjT58886jsUDn5SPHRIWfpZC2dpWbSC/9ELvtQZmOvgt5R8yzs13mOW0ZFMjg4fiI6bP/ojD93g4i/eHOFl64t8q0Hm5tHP/FcMnn01NDJMY+u7VDJGNpAbccdVKT4IAmSQgjODKcppHSK6USI3C23mK20OTOcYbyQOpD9JIfBfp3nuGVUJCeflhdSs/0jYyJdax79sxvL2ANkHpUdKoPNwIiPrbbGzlba3Fu1yVo6mqIQxPDFW2UW6y5ZU+fCWI6PPD3GWN56rJv82g2w06XUug2w+81WWYj98lActQyPZDCJ42T8ecM5OuPPa7bPH7+xxEvXFjc3j45k+OTlST5+gsyjskNF0mVgxMdWW2PnqjZLTY/3nR9mqe7x+nydxYZLGEMhpXZ2rIS9xz/qTf5RMgF7VbrY6rn3y0Nx1DI8Jx1Z5lpPFIuOiTQg6mPUPIzzdM2jf/V2pe+ZMqbG37o4ztXLk1ycPBnmUV1VyVqyQ0WynoGJBlttjT03mmOp6XG30kZTFLKmzmQhxfXFJqaa3EBylv7YN/lH+fq9Kl0cRhZCdqUcLLLMleCHSedKywuPhIn0QdVOJo++sbSpefSFNebRkxCgDU0layUZDtmhIunHwIiPzW7hCoKbC3VqLRcRx5wbzWCmNfJpHUtXeWIsxwunS73A+Tg3+UfJBOyVaHicLMSj3qhlV8rBMuhlLjfodq4cvonUWWMefXUz82jO5MXnJnnxuUlmhtIHfMK9xzI0smbSNWg+wjJOyWAxMOJjs1t4xtK5udRivubgL7cpN30uTRe4cqrImWcSN3nb70wJzZmPdZN/lEzATkTDTsTB42Qh5I36eDCoZa52ZxKpGxzu+HMhBNcXOpNHb25uHv3gEyNcvXz8zaPJiIIkwyE7VCS7ZTDendj8Fh7FAkNTmComt0VNS94gRnJJAO8XdB/1NvkomYCtRENXdKw1zOqq2lccPE4W4iBv1NK38OgMUplLiMREWrcP30RaXWMend3EPHpuJMPVK1N8z6VxSsfYPKoqCmlTS6aMmrrsUJE8MgMjPjZjNJe8EczV2jhBRBSnyVr6nng89oKtREM3I7HWMOsG8Z6f8yBv1DLL8ugMQpkrigVNN6DuHK6JNIoFf32nM3n0dn/zaLZjHn3xmJtHNTURHFlTJ2PKDhXJ3jDw4mMka/L0RI60odIOIp6dzHNpKt8TJUc5jd0VR2sNszOlzJ6f8yBv1EdB8EmOHkFn/HnrkMef31+1efn1Rf7o9SUq7S3Mo1em+MhTo8fWPKqram8lfdo8nj+D5GhztKLpIWAHMTNDWS6M5fi/X1vg1lKbWCgMZwxUVaWYTl6iM8OZI5fG7mYkHD/kwmiWM8NpcimDphsA7FnJYrsb9V6WSgbVtyDpjxtENDqdK4eF40f8ecc8+tpcf/PoaM7kE8fcPNrtUMmY2rEVTZLjw8C/s3eD3Vdvr3JruU0pYzBbtYljwbmxLFGcZD8URTly6caNGQkhBK/NNXZUslgrGLKdm83aDb7AjgXFXpZKBsm3INkc2w+p2YdnIhVC8MZCo7O2fgWnzzl0VeGDTybm0feePZ7mUdmhIjksBl58dIPdnZUmKUNFQdDwQm4uN8inDZ6dLm6a/j9sc6SiKL3g3PJCKi2PMIqZGcpsW7JYKxhaXoAQkE8ZDw1g24mg2MtSySD4FiT9EUJ0xp8fnol0tZ2YR1++tsjsan/z6PnRLFcvT/I9lyYoZowDPuHj0e1QyZg6WVN2qEgOj4EXH91g98EnR7m+2GSp4XJ2OMNUMUMYi176P2tqLDfcdUJjJzf+/RYo/UTETkoWawXDK/ccEPDMZOGhAWw7ERSyVCJ5HOJY0HADGk5IGB+86IhiwVfvVHjptUW+cmd1c/PopXE+eXmKpydyRy4LuhVKd4dKJ8NxHDM0kpOHjBIdLk0V+DvvOcXX7lQQisJoVufsSIbJvMli0+fLb5dZbQdMFVMYutYrDWwXoPe7e2PtGeZqgpGsyUjO2rRk0RVDlZZHywuYq4lOyeZh0bJTQbEfpZLDzipJ9p+wYyJtHpKJ9N5qMnn0j95YYnUT8+i7The5enmKDx8z86jsUJEcdQZGfGwXzFRV5TufHGU4a/KNezV0VcENIhabPl+9vcpKy6XuhLz43CSqKnrfZ7sA3U+gjO25QRPemK8TxoIzwxnOj2b7fr9kCFKDb9yroSmgayojWZN3ny4BD3s+dioo9qNUIltuTy5eGFG3A9p+dODjzx0/4gtvrvDytQVem2v0fUzPPHp5kpnS8TGPru1QSRmqFBySI83AiI/lhsuXbpV7wfRDT44yUVz/xpLUQzVGc1ZPLDyo2gRRzMXJAl++XeHWUpMXzgz1AvJ2AbqfQNlrg+Z0Kc1i3cXUNOaqDqM5q+/3W2l6vDJb5UHVYTRvkbeSYWobX4cuh+m9kC23Jw/bTyaROn0mf+4nuzGPfvLyFN92dujYlCZkh4rkuDIw4uPeqs3bK21KaYOlRpszw5l1QbdfOUJXVU4NZZiruizUXWZKaa6cKvL8qWIvW7FdgB7NmUyXUqw0PcbyFqM5k7sV+51SSdVmttJ+5CxIVzCN5VPbBuqWF2JqWs+vkja0I+vPkD6Sk0HXRFp3AvzwYP0cq22fP+qYR++dIPOo7FCRnAQG8B1963bRMIoRIhk+dnYky0jWYDhr9sTDxck8qrrzX/hyy2e+5hJGMW/MN2h7IVlLR1XoCYW2H7LaDh45C7LTQJ2zdIayyRuspau8+0zpsfwZu/Vl7ObxsuX2eHNYJtIoFnzldoWXO5NH+w1BzVoa331xgquXJ4+FeVR2qEhOIgMjPs4MZ7gwmqXtdQdyZdZ9vpvm77apjqwpXTw7XXzk5+1+37Sp8+pcnbYfMlNKMzOUJmVoVFoelbb/WOWFnQbqsbzFC6dLj+01WbtTZrZik7N0dK3/Tpm17KbctJ8tt9LMun8clol0Z+bREp+8MsmHnjz65lHZoSI56QyM+BgvpPjI02ObBuisqdF0A16Zdchaem/w1mbsNIB1sxJ3yy0Azo1kcYOYlKFxYSxHztKpOyFzNZt2Z1bHbgPiTgN193Fdw+udcvuRgm9vp0zNZqnh8b7zI7hB9JBw2vgaNd3gSCyok2bWvccLu+vsD85E6vgRX7i5zEvXFrk23988OpazePHyBJ94bpLpAzCPCiFYbQfYfkjG1BnOGjv+3ZIdKpJBYmDEx04CdPK7Lmh6AbOVdm+IV783gZ0GsG5WopjWya3aOEGErqq90kj387OVNi03pNLyqTvhvgbExw2+vZ0yI1mWGh53yy1mhh7eKbPxeaZLqSOxoO4gzKyDkl1x/Iia4x+YiVQIwevzjd7aejd4uKRjaArf+cQoV69M8p4zB2seXW0H3FxqEscCVVV4ZiLPSG7zLbayQ0UyqAyM+NiOpM3UYDRn8dU7q1ynScONekHrUW/xvWxD3uLsSPahzEv38y0v8X0cRFbgcYNvb6dMEPHEWFLCOjuSfSibtPF5LF09EgvqDsLMehKyK5sJqMMwke7EPHphLDGPfvzSBMX04ZhHbT8kjgXj+RTLTRfbDxlhvfiQHSoSiRQfPXrlkUobgHOjuXWlhMe9xW+XeTnI7o7Hfa6NHpPRnEm55T9Uxtn4PPmUcWDtu1v9jAdhZj0JrcIb/5u/PFMgbejUneBATKRhFPPVztr6r2xhHv34xQmuXpnkqfHDN49mTB1VVVhuuqiqQsZM/rsz9STbKTtUJJIEKT46rC2PZE0bxw/RNbU3Vv36QoPVls/FqTwLdXfdLT5ragghuL3SeuQU+0F2d+z2ufrdgNfulCm3POaqDrFg3S3/MDtW+gmkjePx9zMTcRJahbsCaqKQ4tZyk7eWWgeysfVexealawv80RtLVO2g72Pec6bEi5cn+fCTo1hHKHswnDV4ZiKP7YcMZ01OD2fIWjqG7FCRSNZx/N4RH4ONQbR7Y98YVNeWR4QQvPqgTqXl8aDqADCcM8mnjF4w3W3XRz8OcqHabp+rXwkB3lk8V255GKrKpenCulv+fvxMO/VSbHzu5YZ7oGWQk9AqbGoqLS9k6UENVVX2tURg+yFfuLnC515b5I2F/ubR8bzFi89N8onLE0xtMhjvMEk6VHTGCimyskNFItmSgRIf/Uon8zWXKBaoCswMpbF0FS+MsXQVIQSzlTZzNZuzwxkEgomixaWpwrrFctt1fewFh2lg7FdCgHcWz9VsHz+KDt1IutufYT+F3nHezuv4SeeKF0acGc6s69zYS4QQXJtLzKNfePPomUd3gtptibV0MoaGesTOJ5EcVQZKfGwMQCtNr/f36/MNlpsemgpvLrUYzhpkLZ04ElTsgKWGxxNjWS5NFR7qmtiu62Mv2E8DoxCC5YbbM/KdGc4wXkj1xM1mJYTux0ZyJtOlZG7JYRpJt+IklEH2EyEEbT9KhGTHRKooCiM58yHD5ONSaXn80RtLvHRtsZdN3MiFsSyfvDzJdx+ieXQzdFVNWmItjbQhW2IlkkdhoN6BNwagsbzFfM1lvubgRxGGpiIQzNUc/CBpIZwppfnAk2PMlpOR7GsD6067PvaC/by5LzdcPndtgTcXW1i6xnPTBb7rmbFedqfh+KQMlSCMMHSVhuOTTxlcmSmsW0Z3EG/C6/8NwQ2iHXltTkIZZD+IY0HTDWm4AUG0fybSrnn0c68t8tU7x8M8uhZDU8mYGllLlx0qEskeMFDio58JcTRn0fJCTg8nQf3GYoMgjKm6AbYXsdL0Waq7zAwlwmLtG2K/gPaob5jblVX2+ua+9vluLTV5c7GJHwrCOO4ZM4F1fpdiyqDuBpwaSjOSS372C2O5Xf8sj8Pa19wNor5G134c5zLIfhBGMQ037LWM7xezlTYvXVvkj7cwj777TIlPXk4mjx4l86ipq8nAL0vD0o/OuSSSk8DAiI/NAmL3BixEklUoWBqOH1Nuujw5miNn6kwWUz2fx1r2MqBtV1bZ65v72ue7XW4RxgJFhaYboqqJ2OlmW4oZgzvlNoYGQRRTzBhEsdg0+7KfJaK1r/ntlRaxgKliihsLTa53jIondaDXXuCHyfjzlhfu2yTS42weTRlaT3DIDhWJZP8YGPGxWUBc+3FVgelSihdOF3l7WWM4YzGcM9f5PLo8yu2+39d0z3Z9oUGl5XFpusBCzX0osO/1zX1tGadqe5wfgVgINE3lI0+N9s6mqQqVlo+hqQRRkn6u2wEjOWvT7Mt+mzvXbiBuuj73Km1mV9uc9XIEUczzp0rHbqDXfuMGETU7Gfu9H3TNo5+7tsCf31zB7TN8zNAUPvTkKC9ePjrmUUVRSBtab8roUTiTRDIIDIz46BcQx7rdLFWbc6M5FmsOy02PkZzJeCHFmeEMZ4YzfWd4PMrtfquW1dWW3zPfbRXY+/EoQmhtGWcka3JqKEMUi97m3m5W6PlTRZpuwJVTRUxNwY8Elq6uazXe6nvvh7lz7QbihhOy1HRQUFAQVDqt0/tZXjlOo9O7k0i9YH/Gn1daHn/4+hL/92sLLNTdvo95YizL1ctTfPzSOIUjYB6VHSonm+P0+znIDIz46BcQV5oe91ZtlpoeS02PvKUxlDVJ6Sq3lh10NXmTmq+5D/kKHuV2v1XL6sWpPMC6Vt6dsp0Q2mxI2FrvxHzNIYwF1xcatL2wZ5wdL6R2nUXYb3Pn2g3ESw2XUsYkbST/nmlD3/dOlqM+Oj2OBU0vpOHsj4k0jGK+fHuVl64t8Nd3VvuaR3OWzndfGueTlyd5aiK/52fYLVpn2qjsUDn5HPXfT0nCwIiPfgHxTrlN1tL5jnNDXJuvkzU1HD/i8zeXWW76rDQ95usOI5kUF6fzXJ9v9HwFWVNDVeD6fAM/ijg9nEYIseWb2lYtqwt1d9MSz3ZsJ4Q2+2Vc652IYkgbGq8+qNNyw8dabrff5s61r2PWSgJKHCtYusq7z5T2vZPlqI5Oj2LRWWe/PybSu5U2L722yJ9c728eVYDzo1k+8dwEP/SumUMfIy47VAaTo/r7KVnPwIiPfgExZ+noqspSw8MLBFZOZ7XtoSnwZGfdfRBH+FHEG3N13lxqUW56rDQ9PvTkCDNDaZabHoamMl9zGM1tPbJ7s4zA42YJsqZG0w14ZbYTjM31b7Tb/TJut9fmqLH2dez+rAfZ8nvUZobsp4m07SXm0ZeuLfDGQrPvY0ZzJldmirzrdImRnMUzE/lDEx6yQ0Vy1H4/Jf0Z6H+VbhC7vtBAQeHiVJ4bC00EsNTwKLc8nhrP8u4zJW4tt3qll2tzDQxNYaqYQlOhmDGotHyabtATH5vVHftlBPYiS6AogNL53w1s98u42V6bo/pLe9hts/1E5GHUmd0gmUTa9vbWRCqE4LW5Oi9dW9zWPPrJK1O863SRmh3u2yTU7ZAdKpK1yJk+x4OjGV0OiG4QAwiiOgt1l6GswVTJQlGSMkUhbTCas7D9iJtLLWw/YrnpcH81ERwPak6nEyRmKGP0jJgHWXdMbv0GT08ku1Xa/npz4Xa/jN3XYeNem8P4pX3UIH6Qwb+f+DnI3TFtL6S2DybScsvjj15PJo/O1fpPHn1yPMfVy5N898X15tH9mIS6GbJDRbIVh305keyMgRUf3WDVdAPcIKKQSkxoZ4YzNN2A+ZpLMWNQt5N09pnhDE+MZblbbjOWT3H5VIm7K8kY9tGcxfWFJvM1B1V9Z9vt2lJH0w0QQmw6wvxxfg43iFhputTtgKGs8VDGYqtfxn5B+zDNWY8q2g7bZHYQ7cUNd+9NpEEU85VtzKP5lM53Xxzn6iGaR2WHikRystgX8TE3N8fP/dzP8dJLL2HbNk8++SS/93u/x3vf+979eLpHohusutM7Tw9lGM6ZKErSTvqg6nQGa6lcOVXkwliKDz81xpnhDLMVG9ePyKUM8mkj8R5YOufH8j2vxMZShxfGfONemdvlxFfxxFiWDz819kgBcq1gcIOIuZqdZF/imJmh9CN0ytSotHzCWPDuMyUuTRV6n9ssk/Coc0622iEDjx7ED9tktl915igWNJyAxh6bSLvm0T9+Y4ma0988+m1nh7h6eZLvfHL0UDwcskNFIjm57Ln4qFarfOd3ficf+9jHeOmllxgbG+Ott95iaGhor5/qsegGq0JapzbnM5o1WG1B0w2wdJXTQxkKaZ2GE2Lpai97MJozyXbadE8PpxnJmtyvOg95JTaWOppuUpsvpU0gmQ76qAFy7S1/peliaCrPTheZrzmkdvAmvVY4VFoe5aZHy49Yabg0HJ92R0zN1xyiOAkCV2YKKIrS+3kSX0Bj13NOvnSrzNsriQC7MJrlI0+vF2CPGsQP22S213Xm/TCRtr2Qz3fMo9c3MY9OFlK8eHmC731ukslDyIDJDhWJZDDY83fo3/iN3+D06dP83u/9Xu9j58+f3+uneWy6wWp2xWah7rDUdMlbOlMli6cn8gznTKJYMJwzyafeqW2XWz7zNZcoFizUPcbyKd57bnidV2I0Z/adZJq1dJaancxHLrsuQO4mk7D2ll+3A4I43lXQXSteWl7Aqu2zUHNRFbizEpAxNTRVXSdq7q3a1J2wJzaKaf2R5py0vJBS2gAU2n0E2KMG8cM2me1VnXmvTaRCCF6dq/PyNubRjzw1xtXLk7zrTAn1gDMMskNFIhk89lx8/O///b/5xCc+wd/7e3+PP//zP2dmZoaf/Mmf5B/9o3/U9/Ge5+F5Xu/vjUb/XRB7TS9YuT5pU8ULBJW2z6sPajw9kd80kLW8kDCKSZs6d8stiul3fBLdwNPPfDiWt/jwU6OcHckA9DbkdkXHbKXNvVWbbKf9d6tMwtpb/lDWYGZod+vs14qXuZpgNGcxX3Oo2QF+LChlLTw/XidqgHViA9h1piFnJQPAlhrvZD5240/ZisMyme2V0XWnJlIhBKvtYF1nSb/nW2l6/PEbW5tHn57I8eJzk3z3pfF1AvsgkB0qEslgs+fi4/bt2/z2b/82n/70p/kX/+Jf8LWvfY2f+qmfwjRNPvWpTz30+M9+9rP8yq/8yl4f4yE2M1bODGXIpwwMLdnt0nRDbiw2uTRV4Pxo9qE39u7CtVfn6snfV23OjmTXCYXNBMpEMc3EhiVaXaHyYLXNnYrNpak8Kuq6tt2NjOZMpkvJXpq149BXmh53yu2H9sZsDIxrxYuuqpwbydDN7L+x0KDW9pguZdaJGiEEdafRExtnhjPryjA7ET1jeYsPPTnKmeH1Auw48zhGVyGSSaR1e+cm0tV2wM2lJnEsUFWFZybyjOSSLpMgivny7QovvbbI1+72N48WUjofvzTB1cuTPDH+8Ebi/WJth0rG0NCl4JBIBpo9Fx9xHPPe976XX//1Xwfg3e9+N9euXeN3fud3+oqPz3zmM3z605/u/b3RaHD69Om9PtamQeLMcIYnx3K8+qCOHURoCizWXIJI9A0kSTtqhrYfcm4ki9NnGNdOBEqXbhZiKGvx1btVvDBiLJfiuZk8S3WnrzlzbelnvuYymksC+GZ7Yzb+zBvFy3DGoOFGhFHMlZkiZ0cyvfHqXfElhOB5RaHpBnhhTMsLyaeMvgJtMxRF6SvAHoWdZhz2uwX3UYyuj2Mitf2QOBaM51MsN11sP6RRDnj52vbm0U9emeSDTxyceVR2qEgkks3Yc/ExNTXFs88+u+5jly5d4n/9r//V9/GWZWFZ+3/73SxIjBdSvO/CCF4UU254hDGMFyxWmj5vzNcptzxMLelWaXth5wankjU17laSLMPGiaI7EShdulmIWttjPJ+MV1c6fojrC82+3TFb7YiZLqWZq9rMVtrYfsRqy+fiVJ6FukvTTQLTbKXNbMUmZ+nM11xGsua6MtNozqTc8tdlUdbORLnzGDf9vRICO8047HcL7m6MrkEUd8afP7qJNGPqqKrCvdU2ry80+M9fmeXWcqvvY7vm0U88N8nEAZlHZYeKRCLZCXsuPr7zO7+TmzdvrvvYm2++ydmzZ/f6qXbFVkHC9iPSusbZ0SxvLDT46u0KXhhzu6LhBzFThRQLDZeImKxpMJIxUFUFVVHoF0O680JmKzZ3O/tjugJlYwAezZm96aK5tNHzfCiKsml3zFY7YrozRRbqDm0vZLWdzBcZyVt4YcydBzVuLCblk/edHwElGVJ2YSy3pWelG7Afp6V1s7beRwlQ252j+zpfX2isE2B73YK7E6OrG0Q0Op0rj4MQgvurbf74jSW+/HYFv0+p5jDMo7qqkrVkh4pEItk5ey4+/tk/+2d88IMf5Nd//df5kR/5Ef76r/+a3/3d3+V3f/d39/qpdsVmQaK72fZOxWa50/HS8AKCSGDoKvN1h+GMTtsLMHSVKBLMVR3OjGZ4z9nhvhNFu/Qbeb52HXzLC3sljm87O7SuY0YIwWzF7tsds9l4724ppe7AStOlmLGIhE/KTAysTTdIAn8kqLQDvnJ7lfeeG3rott50A1ZbPoW0zmoroOH4AL25IqrCI7W0Jq29Pk0vpNz0EEJsuw9nM7bLOGyc4wIwnDP3vAV3K6Or7YfU7GSI3eOw0vT4w9cXefn1ReZr/dfWPzWe45NXJvlbFw/GPGpoam+pn+xQkUgku2XPxce3f/u38/u///t85jOf4V/+y3/J+fPn+a3f+i1+7Md+bK+faldsFiS6A8IuTeXxwoh3nS5RbnrM1VwsPdlc2/ZiFEVhvu5gaD5DaQMhkgCsKsnN9vZKa10pYbOR590be9rUeXWuTttfv0G2ez4hRN/umM1+lpWm1/OBlJse7SCiRNLeO11KM15IJZ0SnbbaM0MZCimtr+nTC2PuV22CcoyhqUwPpbhbcTqZEHbdXdMlZ+mEnfON5S1MTXvkTMR2GYfu63xpOhmYNlG0uDRV6D1uv7wgj2Ii7UcQxXz57Qqfu7bI14+IedQyNHKmTtrUDn1jrUQiOd7syySm7//+7+f7v//79+Nb7zk5S0dTFJpOiB9GvDHXIJdSmSxYFFI6xlSBU8UUq22DtKEwVcqQT+k8MZZjNJ/CDaJ1w7i6ImKzm3lvg2w5qdOfG8niBvFDQXi35sy1ZYia7aOoYBkqT+Syve4SgJShoqoKQSSYLCZZF0VR1gXjlhswM5SilDGp2wFhFK8rcaQMjQtjuw92Y3mLd58pIYTA1LS+o+B3ynattd3XeaHmMpJLhMfaDMteloAg8ds03YCGExLGjy467pTbfO61Bf7k+jL1Tcyj7z2XTB7db/OooiikjCTDITtUJBLJXjIwu1363XS7H49FzP3VFvcqNg3X5/RQliszBTRNJWcm49bn6x6xolBzQoazFudGc4wXUtxeaRHFPOQ92OxmvnaDbG7VxgkidPXxN8iuFTvDWZMrp4qdWQpJSvz2SotKy2OykOLCaI67lTbnRjOM5kyWGy53yy1en2+gKhDFUEjrKCiMdMoi8zWXuZpNuzMV9VGyBYqicGmqwGjO2vdhYDvJjOxFCahrIm25IfEjmkhbXsjnbyzzuWuL3FzsP3l0qpjixecm+d7nJvbVPKoqCmlTS6aMdsytEolEstcMjPjY2PVwZaZApe3zjXs17lfaXJtvstz0iEXMg6qDqas8KRQiAWesDLqmMJXL0HADCunEKAqbew82u5k/ygbZnZQINgbbbsfK2uFlrc7NXFMVspbOmeEM5ZbPqw/q3Fio8/pCkyfHskSx4NRQmicncmRNDSEEbS+kavsIAZWWv65UtBsOahjYTjIjj1MC2omJdKuBYEIIXn1Q53PXFvmLN1fw+kweNXWVjzw1youXJ3nX6f0zj2pqIjiypk7GlB0qEolk/xkY8bGxO+Leqs3NxSYPqg52EOH4EQpgKEkQqtsBY4UUi3WHctMljAUPajZZy6DhhJRbfk9E7Gas90YhsZM5GTtpF90YbLsdK3NVm6Wmx/vOD1OzfbwwImPpPRNs93UZzaeI5xv4UYymqgxlTS6M5VhuuL0dLkt1h1U7oJQxCWLBuZH0I7et7vf8je141BJQ2wupOzszkfYbCBYLsa159JmJPC921tbnUvvzK6qram8lfdqUhlGJRHKwDIz4yJoaTTfglVmHrKUzlNExNY2xvMWdlYCpksVSTWAHMTrJTXB+1Wa8aDFTTLPYcDtGzTRRJGg4PkKIdUPAdhJAdzp3Ym1wLjddyi2XUsZMSgVbTD/t0hUV50ZzLDU97lba6KrKSDbFpel3TLBJ5gYajk/O1FEVhQujmZ5PZK1oe2OuzqtzNQxNw9QVLk3meXKi/5nXZl/6CYz9nr+xHTspAXV/noYbJMJUUwl3MRSsOxBsOGPyxVsr/M+v3efafH1z8+izHfPoI/hpdkK3QyVjarIlViKRHCoDIz6SLoSkhTRGkDFzDGWTlsSnJ3I8M5njb2ar3Fu1iYWglE5S5NPFFE0vZLHu8dZyi5VWsgsmk9Jw/Zg7lYeHgG183rUBudmZarndnIy1wXm+ZnN/NSkFGZrKlc700n4/Y/e53CBCU8HxQy6MZjk7kiFr6cxVnXUlorG8xXQpzULN4dJkActQeHb6HSGwtqwEMcPZZPHeg6pNuKGbY6OgmC6leh04ezkvZK+yJtuVZhbqDl+9vUrbi0Bh3SjznbDS9Hj59UW+eb+G3acd+x3z6BQffGJkX8yj3R0qskNFIpEcJQZGfNyvOqw0fUppg5Wmj+1HvHC6RMsLcfywV3c3NJWWG3BrpY0XxYzkTVbbPlEcUXd8IAZS3FpsYOr6Q0PAxoRgueH2MiIZU2O+5hILegF5JxMx1wbnhbrNcNbgyfE8DSfE6hNEhBBcX2jwymwVU9MoZXRODWceaondeNNPOho0xgvpdd0s3WC+tqyUtTTi2xWqtk8pYz4ktDYKipWmt6nA6OeV2amo2C5r8rjipLvO/tZyi6YbrhtlPsJ68bHR12FqCp9/c4WXtjGPXr08yfc+O7Hn2Z61O1Sypt5bCiiRSCRHiYERH0IIbC8iikTP3NcNyK89qHG73MYLImYrbYSAfNqg4QS8vdRE11WCUFBt+wQRjORAoKKqUOsM4OoOAVtpenzpVpm3V5KMSD6lM5KxeqUOS1cfMoYuN9wtl7/lUwY5K8nEDOfMvkOkVpoe37hX40HV6f1cT0680xK7VUDeamDX2uzAuZEMw1lz3UK7tWz8PmN5i/ma2/f79vPK7LQUs13W5FFLOo6frLO3/cREmjaSbo/lpovaGRu+kdV2wPXFBrdX2nzjXpU3FhoE0cN1la559JNXprgyU6Bmh9h+0nGz2WbanSJ3qEgkkuPGwIiPtKFSbXtU2x5DWYu0ofaC1P2qTauz90TXVHRVwQsjFBSCWGApClXXp5Q1KaQMohgsI1kJrygKpYzRW8R2p9ym5YWU0gag4AUBlbbLK7PJMLOcpfc1hm4MlOsyDh1DYNej0c/U2vJCdFVhtBPELX19++76gJy0BnezIt0R79uZZlVV5dnp/iUf6N9xs5mnol/JY6elmO2mm+62pNPqmEg3rrMfzho8M5Ff162yluWGy//8+gM+f3OZqv3wTA5IynEfenKUjz0zzunhNIqiUGn5m26m3Slyh4pEIjnODIz4mK+7NL2QlGnQ9ELm6y7ZlEkUCy5PF7mx0KDS8jp7WFRW2xGWnqSwz49kGc1bPDmRp9zySBkqacNAoKCpam/mBySBMWfpLDXaCCFI6WoS3NyAQkqn3PJ622mTEept5qptSlmLWtujmF6/yG3txNNu5gJ4qJSQs3RGciYCgR9qmJrK3XILIcRDy+jemK+zWHcZy6d6bcc7DV47MZWuzTJs/Bn6ZXnW/gz9RMVm+3A2E0s7WfYWx4KmG9JwN59EqigKIzlzXanFD2P+6u0KL19b4Gt3q/Szn3bNox+8MIIbxsSx4EEtMTqP5My+m2k3lnP6YWhqMn9D7lCRSCTHnIERH44fEccCXYeaHbJYd3jX6SFUBd5cauKHMaWMiaGrtNyQrCUQioKhqVwYz+GFMZWWz1g+xVjOJBYwM5R56GY9lrf4zidGyKd0Fusuiw2Hhh1S9wIWUFBQGM1ZTBTTrDQ9Zis2t8s2K3dWGc+nyVpJFmVjmWC7UkKSdSgl22y9iLuVNndXbZ4Yc/jwU2PrAnIYJ+2la9uO6064ozLFbkyl231t/5+h//6dfl+3WTZjNGf29tyM5a3eTBaAcM1m2d0MBXt7pcVL1xb5kzeWaLgPz/ZQgBdOl/jBF6Z6k0fvr9rMVuyHREZ3M+1W5ZwuskNFIpGcRAZGfIzkLCIhuFNuo+sqNTsJIDNDaV6fr1NImahpaPkRKTNmJJflzEiW4ZzFVDFFIW2uW/r22lxjU4+EqqooKDScgPmqQyjg3qrNVD7FYiNZZDdRTPe+36WpPLW2x0hGY7Xl8Ve3VpgZStpdu1mSbuZiqpTi+nyD6wsN4OEMiO0nM0uKaRNFoWeEPT+a7QX208Np5qoOc1W70xkT4gYxl6YLLNS23vy6G1Ppdl/bb6T8Zvt3ul83V7OZrbS3NJOWW35PEM3XXEZzFsWMQd0JaHvRjtfZt9yQP72xzEvXFnhzqf/a+q3Mo5uJjO3KOVZnMm3G1GWHikQiOZEMjPiYKqa4PF1kte2Ts3SKaYO2H5EyNKaKaXKWzmzFJhIxGSNNxlRpuiEzpTSFdNLZ0e1kma20iUXM0BqvR5duKeXmUoO6G+AEMbYfkdZ1Tg1lMI13gknO0tE1laYXEQm4u+pSd3yGcyajuTYXRrN85Omxdbtirs83eFB1Ej9KVO9lAdZuca20fWIBGUvrGWHXBvbuKPHZSpu2nwiP7ubXkZy1ZefJ2gyKqiTeg5WmS90Oth3UtZNyyHZf1/ZCWm7IajvYNNOyVqzcKbe4U27veIx7LATfvF/j5WuL/MVbZfw+k0ctXeXDOzCPbiYyNpZzujtUMqZO1pQ7VCQSyclnYMRHPmUwXrSoOQGRgKz1TqDseiXyKY2a47NUbxEJheGMzvsuDPfS9itNjy++VeZ2+Z3ZHudGc+tu3t1SylzVZbXtc2Y4g6YqqIrCeN7qpdBvr7TImhpXZgroKhALDE3hlftVhtIGpbSZBNoNu2KuLzRQUHhmKseNhWYvA9KdH3JpuoAQgrSZlFX6ba3tCpGWlwTxqWIKBWXd5tfNSh1rSyNuEDFXszE0lSCOmRlKM5a3NhUuu50G22Xt11VaHpWWv2WmJWtqeGHEqw9qCGCquL2fZanh8kevL/Hy64ss1DeZPDqZ55OXJ/nYxfHefztbmUf7eUbW/htkOjtUMrIlViKRDBgDIz6EEIgY0kYyvfTSVK4X/LpeifurDqstr2cSDCKdt5ea3BjLcWmqQMsLaXvhQ7M9NnZs5Cyd918Y4Su3y+iqynQpxfmxHFPFFF4YP7QF99JUgXLLZ7HmoAiFxYaDGwouTxce2hUjhKDc8vjiWyustpIOiyASTBUtmm7A4qxNLODCWLaXldnMTNqd+rpYT8yQFyfz2w4BW5tBub3SIo6ToWRr54Ns1sHzqHtd1n5dztKpO2Hf7EkUCxpOgBNETBXTFNMhGUMDIbi/aj+0XyUxj5b53GuL/M1sf/NoMW3w8UvjXL082XeT727Mo3KHyu457DH8EolkfxgY8XG/6lBu+0wWs9QcHyeIex0n0N3Z4REKQdsN8EJBxtS5X3X5m7urjHbKEVlLZ6nZyXx0Shpr6ZZSAJ4az+MGIV4guLPSYjhrYunqQ1twM4aaTF9t+6AIJgspdE2jkE7KH0KIdW+4QoAXxERxjKWrzFVtojgCkg6OB1WHlhtwb9Xhw0+NMlFMb/q6KAqgwMb38+7Y9Tfm64Sx4PRwuneObkCotDxaXsBcTazbzPs400u3o1/2pDsUrOWFvX/PbsahX2aiZvuJefR6f/OoqsC3nxvm6uVJPvDECMYWZZDtzKNrd6ikDFUGzl1y2GP4JRLJ/jAw4iPZzBoQRTFu+I7psPvmdqfS5m7ZZrHu0e4EpCCK8MOIuZrDbKXNt50d4sNPjXJ2JNl7cmb4nZX0TTfAC2NMLekAsXSVkZzJ7ZXEHDlXc1Hv1XjX6SItL+CVe04iZkyNe6s2K00fQ1dxQ8FoPk3VDlise7ymNni+c/OHzqyPlM6TE3k+f32JL9xcZqqUxvZDhrMWo3mL1+YbFNMmt8ttznRmS3TPZ+kq+ZTBWN7qzA0xeHrinV0vXcbyidH2zcUmsRC8PldnJGv2unRefVAnjGKEgJGsyZnhDEIIbq+0eqPdd+vt2AlrsyCOH7HU8HpDwbr/zmsnjrb9gDgW5CydP7uxzP/7i7e5W7H7fu/pUtc8OrlpSWjj9x/q4+uQHSp7x34KWYlEcngMjPjImBp+GLPS9CimDTKdwV3dN7dTpRS6qmBoCoWUTiBiCqlkJkjNCbi3anN2JMtEMb0uk9AtMVRaHg+qDqeHMgx35lDkUwY3F5usND1G8xa6qtD2QoQARJLBWIula6iKwmLNwTJ1zo3mcIPoobHkbS/k7ZUWipp8n4uTedwgJowFVdtDVRRMTaHhhdxYbHK/6hBEMXNVd935tptsavsRLT+ilDa5U7E5t6ZLJ4pFr9V4JJekwrs3VFVJuog2jnbfC4QQvaFg/cygazfJokCt7fMnN5Z5Y77Rdymcpat819NjXL08yfOnittmJvptqh3JmUwbadmhsg88qklZIpEcbQbmN7nlBjS9EBELml7Ym2iaNTVaXsBC3SEmCUalYorFmkvNDihkDC5O5PDDiL+6tdJbP15KG+RSRq/8UEjrBOWYQlonikWvvfU9Z4dQFAVdTcyHiqKQTxk8M/lOtuH0UJrRrMlqy+PiRI6LkzmaXkzb9VlseDh+wHzNYbJgkU8ZnB5K03JDnpnIc2OxSc0OmC6lmRlK03IDsqZOywvQAoW6k3yPM8Npgujh8+3MALo+aPcLCBtvqClD6+uReFTiWNBwAxpOSBj3HwoGiQdjtZUsAfzirTK1TSaPXprKc/XyJB99ZnxXAW2tx6Nm+1i6ypnhzEMdKtKrsDc8qklZIpEcbQZGfNxbdZit2Kgkq+HurTq8H4jjmLmqzd2VFqaqMJo1cIKIIIywFUFkw5feXsHQkoDbcpP9LnnLoJQxuDCapelFFFwdP4q5tdJkqpjcgvutbRdCUHfWzwgRQlDMGGha8vfnT5WoOiFfv7vKW8tNNFWhZge8cKrEUNakkE68J6au8uR4jjPDmZ65VAhBLmXw6oMaKT3kyYkcX7tbpdzyMDSVhhMylDVwg4g75TZZU+sIsIcnp54ZzvDEWDYRKpkMbhDx5zeXGc2ZXJ7OYwfxuoCw2Q11u0C81eeDzlCw1jZDwfww5i9vlfnf35rn1Qf1Tc2j3/vsBC9enuT8aHbX/w0pSjIgruEk3pLRvMVkMdW3NVZ6FfaGRzUpSySSo83AiA8niDotqDotP8Tp7PF4bb7BNx/Uk+FcXsBozkQJBYamMdYpJ9TbIflUkr1o2BF2EOIFEau2x3QpRSGlM5Y10BQFVVlfTum+eY6tCbBdT0jXe3Gn3F7nvbhfdbi36vDmQpOFusvl6SLLDR8vjLhdbjORN8mlDEZz1kMdLStNj/mamww5c0OW6x4XRrOcGU6TSxlYuoobRLwxX6ftR8RCkLcM8injoSA5Xkjx4afGaHlJd8lX3q4QxgJDU7l6ZXLdnpetbqjbBeJ+ny90Fvt1RdFmvL3c4nPXFvnTLcyj33F+mBcvT/KBC+vNo13/RtsP8EOBqStkTWNdR4yqKD3DaLJDJflZt7uJS6+CRCKRbM7AiI/JYorxgoWuqGRSGhMFi+WGy51yi2orMXsKoGoHxICiwv2aw1Da5OxoholCijfm66y0HaJYIQgFigJvLTU5N5rn7EiGtGUmUzirNvdW7aSNteERhIl3otb2MXWNkZzJ86dKvdZZN4gotzxqts9IzqRmB9wut7EMjYYb8tZSC1NXaHsRiqp0vCAxIzlr0wFbl6YLvZ+7O7ujG1C/dqfC7bJNKW1wp9xmppTqlYHWBsm1t877qzZhLHhmssDNxQYrTW/d8251Q90uEO92KFjTDfjT68u8dG2Rt5b7Tx6dKaUT8+hzE4zm+n+vrn+j3g5YaDhMFlKUsibPTRU4NZwhZ/XvUNnJTVx6FSQSiWRzBuYd8fmZIjeXmizXPcaLFjOlNK8+qNN2IrwwouEFKEDe0kjrOmpKAQHPzeSZLKQTE+FUgVLGpG4H1J1EsFw+VURFxfZDIgFztcRP0fZDvvkg4Fv3qqR0jXLLYyRn9YaAdUeEu0HEg1UbQ1XxwpCUoVFuurS9kMm8yYXRDNOFFKdHsuRTGi0vwgmida2ta+kGvYWamzzfVGGLdL+CpWtomrppkOyWRLwwwg9jbizUMXVtV7X37QJx1tRwg4i/fKuM23kNRjv+mG52ouUF3F5p85e3ynzxVrnv2vqUrvJdz4zx4uVJnp/Z3jza9W/kUhpxTTBRtEjpOsWM8djeAulVkEgkks0ZGPGhKAo50yDICHKmge2FVJoeupbU8aeKFmEElbZDuR0RRRFnRrOcHc5RSJt4YcgLZ4bILDSYr3s8MZ7FCWL8IMaPYgrppGwxkjUZzhjcLrep2z5NN2RqPMVKy8PUYbbcJmWoOGEyCKvS8jFUlUvTBa7PN5it2Ghq4p+IgEuTedp+xL1Vm6GMwbefG8INRUcUJC2+3fLNZlNEN3oqTg+luTCape2FPDdd4NnpPGlT7xsk15ZETo+kGc6YPDWR5+Jkfsev/WaBeO1QsLSp4UWJqFpuugxlTEZyJjcXW/x/XnnAK7NVas7W5tGPPTNOdhcZhlLGoNpO/lsYyVnEMaRNbU+yFNKrIJFIJJszMOKjO2SslDYpt31mV11uLjW5vdKkavuoSjLn4VQpy6khBZRkA2rN8XlqMs9iXVC3A/Jpk7QTcno4w1AmmengBYKLU3kW6km2wQ0i5qouC41k4uV83SVn6WQsA8+P0BWVlYbHhdEcmgrllssrs8n01OGc2fNSpA2NB6ttvnm/RiljcWulnWQRNJWFukOlGTBdSpE2Nd5zdohLU4W+QW/jxNHL03menS70tr5enMyjqv27Na4vNKi0PC5NF1AVlacmcrvuYtl4pmQomL9uKJilqwxlTMbzKeZqNn92Y4m/vlvllU0mj5bSBt/zCObRlNGZMGpp6KrCVDHddwbKXiI7XyQSiWQ9AyM+ukPGwjCi6gQULRVDVxjOmlTaPvcqNnYQUUobZCwNU9dQUWh6EV+9s0re0hjOWjx/oUQhZZAyVaaKKdwg4rW5Bss3bdKmTimtUXMCLB3ee6bETDHNZMHkzEiOIIxYbvhYhspX71T40zeWMHUFVVVImyppMwmICzU32ZcSCSptn6odMpZP0fYFD1ZtQhSiOOZOpU0kIjKm0evE6JZY1ga8SssjjGNmSpmeobXuhOu2vm4szSw3XL50q8xC3WG1HSAQjOZSj5UVcPyIuhP0HQpWs31ul1v8n9fmee1Bo2cIXosCvPtMiR961wzvvzC85eTRd763T6Xtk9JVnp7IJ3ts1gT+8UJq37tQZOeLRCKRrGdgxEfG1HD9iDdWarT9CF2BSAhuLjWYr/soIiZWoGH7PDNZQFMS/8ez0yVqdrLITFUVFuoumqawavvcr9o8WE12qQSdaZ/LLZe6HWJqKiutgMmCxbvODHNxMpnJ8cZ8gzsVm5WmQ7UdkEvpWLrGs9NFLE1FUxUsQ0UhmcdxZabEzaUWCzUnmeUxnGZ21SGMkjHwi3X49vNZdFVZZ+RcG/CaboCivDNxFNi2E+Peqs3bK22KqeQcaUPj+VPFXWcFthsKNlux+f9+Y46/uVtlodF/odt43uK7nh7j45cmeHI8u23WQO0sbWt7IXcrbe6Uk4mm5Zbf2xJ8kAxa54vM9Egkku0YGPHR9qLOLThps52v2YxkU2iKgq4I3BCqbZ9CKpmBEQsFiHhzucWF0SwvnC6hKEmAv7Xc5O3lFi034K2lFufHcmRNnTuVpEOlbvs8PVGg3HKJhGCuZrPa9rlbbrPU8FhquChK0lmTNlQW2j5/cWORy6eGURRQVYVYJN6UKI45PZQhY6qcHs7y7FQB2495c6nJZCmNoSbG2JGcuS4rsTbgzVVFsuuks5+m36yRzVAUlYypkTbemQUymjMpt/wtg0scJ3tm6k7w0FCwWAhema3y0rVFvrSJeVRVEtHx3HSBH3h+iiunSpsGsOTnCYgFjOUsTg+nUVWV2ystbD+imDJoeSGz5TazI5kdnX8vGbTOF5npkUgk23Gy3wXXUHN8lpsufhSjqVCzI8YKCpPFNJBsYo2EIG2oLDZcdFXlfU+MoCGSOR55C1VVGQcqLQ8niKm6AV4Uc3ulRdbSQEkyLE1Xoe0FlLIWV2ZKLNZdFmp13FCAmrwhu36M3dlNIhSFCJVK26PphhTTJuWWx3vOlJgspni3MsTFqTw3F1pU2gEzpTSqAmdHcyzUkm2txbRBHMcs1ZOpqW4QoXayHbqm9uaBdG+kU0WLthf2Oko2Lq87M5zpmVLHchaNjtDS1GR3zXzN7Rtcws5QsGafoWCLDZeXry3y8rVFlje06nYZy5lkzaSbZtX2CaOYhbrHzFDQW1XfLdV4YcRoziJtqKy2A6JYULMD0qbGeCEpEeUsndsrNZabPuN5i3urNllL3/T8+8Ggdb4MWqZHIpHsnoERH6zZyBrFUMzofOTpMVYaHq/N1RjOWoRRCKj4UYwbxrz2oMaF8RxNL6Tc8nsB6sxwhomCieMHPH+qQLnpM5QxcPwYRMzFqQJPj+do+0lbbBgLcpZJLg1vzLt4QUjT8UkbKpoqyKYM3nduiFUnGaNuBxEtN6Tc8nhupkgYw82FFverNoIkYOZSBq4f4QYx1baDFwjultuoqkLOMtDUh/errL2RtrwAIZJb+WzF5uxIZt3AsvFCio88PdbzjFTaPlPFFDcWmpRbLipqz2Tb8kKKYeLnaHsRcRz3lq/pqsobC3Vefn1pU/NoMW3wbWdKTBYsFuout1ZazNeS9uMnJ/JkTK23ql5XVRw/ZKXpJSIucCh2RsZvDHZjeYsPPTmKrircr9pcni7idvb7HGRwHLTOl0HL9Egkkt0zMO8KsegEbUsjFvChJ0f4vitTlFs+z58uIYRgse7w/3t1gWYjROusmb84nieMBNcXGkAS0MYLKb7rmXEK6RpV26OUtnh6Is837tfIpXSmi2ne1SnT3Fu1URWoqQFV28P1Q2IBQlEopA0MLVnD3vIjJgspHD+m0vK4OJlnOGNh6SrPnypyfaFBLGImiilmV1pMldKUMgZ3yi3maw6LDYe6EzCSNfnI0+O4YUzK0Dg/mmWl6XGn3E6Mp1HMzFCGV+45IGAsn+LVuTptL1met3ZUezdg5iyduhNyY6HJ/apNIWPQsBN/RjaVCIO5qtN7rVfbAZ+/uczfzFZ59UG9r3lUVeB950c6k0eHWai7fP3uKmEskmm0gKkppHUVy9CYKqaYLiVi6vZKNwOTiAfoP9pdURQmimk+8MQo2Qd1vFCgqwqaqrDSdKnbAUNZ48gHx+PmoRi0TI9EItk9R/tddw+pO0HiP4hidE1N5nJoWi+bcW/VpmoHCEBRBKt2iO2HvDZfZzibpPuDSPRS9N05F28tNam0fGbLTRpOwKmhNFEsaPsR+ZTR6yppB2GSFUCQNpOOGBXB2aEspazFeN7i/RdGuDTl8417NUxNo5TR8cIYxQsZy1ss1V2+cGMFL4yIEAjSyQj1lseDVZsgFoznLSLg+ZkSOUvvm+2YrzlkTY2mG/L1uxXafkAhneUb92q8PldjLJ9kPZ6dLna6aEymSynKLZdCxuDbz5Z49X4dXYPxnIXjRVRaPlEs+Ma9Kv/Xtxa4t9p/bf2poc7k0WcnGM6arLYDFuouXhiTNnSCCExdZTRv8eRojnedKfHkeH5dwN14sz4znOn5cfoFu7XBMGmDtpNuojhmZih95IPjcfNQDFqmRyKR7J6BER/lpofjx2iqguPHlDueg5WmxxffKnO73Ga+6tB0AgxVJ458IlXl7kqLsdwIF6fy3Fho8sZ8nXLLo+n4vD7fRFEEi02X5ZpDuRXyda/KzFCKmaE0D6oOlZbHRMFipenj+SFNN8ILYsIoRlEUnCjmVEojjEFV1d6sjuWGS9ML+Zu7q5i6xnDOAAX8KPE5zFZsKi2fuu1TtX1MXSWnKUzkU+RNnTPDmd7emJ7xtCYYySbGU8cPeWO+QdsL0TyVm4tNHlQdMqbGfN0DRWEsn7Shlls+8zUXhMJK3eXLb6+STxucHs4SC8FL1xb5+t1Vri82iTZZW/+xZ8a5enmSyzOFnoiotPw16+mTabKXpnK4YcxI1uTsSJbxQuqhW35XDHXnlKz14/RjbTC8vdIiEsnY+buVNu1tdsccBaSHQiKRnDQGRnyINf8XRO//a3khLTdAQ0FFEMURi3WXphsyk84lWQsv5PM3lnl7pcVkIYVlqISxYLbiMF1I8dZKExVBytJIGQphJLhTbmJpBg+qNnfLbequzzMTecptj3LL48xwhjgWGGpiem25AbOVNnEcc32hwULd4fZKm4ypcW40GeqVMlRKGZN8yqDc8kjlVN53YYRV22Op7uHHUHMCrpwqcnYkaUldmyXQVbUX0G+vtCikTZ6ZLHBjoUnd8SikdFRFJWVCFMW9IFd3gl6JotgyGc6a5FI6//tbc3zutUUqbb/vaz6et3jPmRL/z28/w6mRTM8oavshGVNPPCEKlPIpFuqJcfa954a37GpZaXrMVtrMVmxyHeNovzklG7+m5YVkTQ3HD7mz0mKx4ZExk4Fj3dfkqCI9FBKJ5KQxMO9io1kTVQEviDB1FVNTuL3SwvFDanbAX9+t0PZD4ijGCWLCGB5U24xmTEQsuLXcpOEkI9kVVeGJ0SxxJGh6QWfGh6BScyllLDRNwfUFz5/P8aBqs9KwUVSVt5YaDGVMsqaOqiikTI17lRafv7GIisLdSpuLkzmuL7Zw/YiFustT4znKTY+0ofHEWJbVts9q22eqaCUekSDiVCnDeC7Jtqy2k2mtd8sthBCb1t97O2DqLsM5k+dm8ui6yltLLQxNZbKYxtSSUed1J8AJI2pVn7vVNi+/schrc/W+r3PG1Hj36RKFlM77L4wkJt+OllhtB7y13ERXVTJmyPnRTFJSmKsBSelrKyHQLT/M1WyWGh7vOz+C44e9PTn9/BAb552AIBKCIIy4eGYIS1ePfCZBeigkEslJY2DER8tPTIyaphJGglvLbc4ttWi6AX4ckbU0VAWcIETXVDKKStsLaQUhFdsjpetoWYWv3K5ALKi2fSYLKeJYRwMqTjKiu5jRmClmSJsaNxdbrLY9hKICggdVl/G8RcrQaLshD6o283WHuh2QMXVmVx3ulluAgqKq1NoetpfmqfEc7zpdRAiB40dYusp43mKikOL1+QaGphHFCqqqEsRwu2yz1PQ5P+Lw3ExhXcdLNzBvDGijOZPRnMW9aRs3iCimDbwwWbq30nT54psrfPFWGdvvbx59/lSRD1wYZbJo0XRCFhsuLTeimDUopAwKaQMviBjJWp0SkE3bCzuGW7XXibKVEOiWH86NZFlqeNwtt8haOm0/pNKZ27Gxa2dtyeKVWQcUuDJTwvZjarbPzFDmyGcSpIdCIpGcNI72u+4eEkYxuqJiGipNL8APo15ASuvJnIzZio0QgjhOJoCWsiZjeQs/FKhGzFzNwQ9jRrIGyw2HlK4yXkjKIF4skj0vfkzKUDg7kuHGQgM3iPHDKNnFYvu4YUwQge35eCH4UUzDDQljQcpQWW37jORSmCoMZU1KGZ3zYznaXsjf3KtSd0LGCxagUrUDml4iFCrtNitNhzAUpHQFTVFYqjv4UcxozuoZFcfyFssNt2cI7XpDANKWzmQxTdCZ1fH735jjpWuLvL3S7vuarjWPjuSsXlml7Qc8GxcopnXGCylODSWG0DgWzNXcxLfgBizUHNp+yGo7YKnh9YagbaRbOqm0PFpeQCySLNCZ4QwAlbZP2tCTrh0/pO6EPVPm2pJF1tJRFHCCiCfGspwqpQli0fPx9NtxI5FIJJK9Z2DEh1AU2kFIzUnMjWpnjXzW0qk7PnNVlyiOKaZ0cpZG3fZQVQXXC1EUNemoQNByQ1RFIYxDIgH3Vl2cIMT1IyxDI5PSGS+kWWq4PKi5LDUdoliQs3SWmy5LNRcviomiCE3TMXUFEAgBKUNnOGMyVUwRA5dmimRNnbsd0+hiw2UobbLS9LB0BUXRqNk+rh/R8gIsTWGh6RJGSUfNRN4CNREJThD1JpR2DbYA50czvHBqCMtQ8cOYV+5Veem1Rf7y7U3W1hsqH336YfMogKlrPDlukbE0LF176GvXZltuLTV5e6VNKW0SxR4pQ+2Jo42tpUIIXptrEHZG2I/mrHVD0+pO2MkYwbmRLG4Qr5v10X3OrJmcqe1HnU4gl5evLRFEcW9PTHepn0QikUj2j4ERH0MpnTNDmU57p88z4zmemsiRMVT+8PUAQ4OJQoaK7UIMp4bzVNoeQ2mTJ8dzTBZTTJVStL2Ym0t1FDUZVOZ4EYaiUiya2F7IE6MZCmmdaw8aqCRzNBqOj6ooqIrSGyCWNlRs10dRFNKmwVhO5/xIlotTBaZLaSq2z1DGJIwEpqYxMZxiqeHgBTGWngwhWao7hGHMvbqDbmjMjKQRCkwULNp+hCLA7izGe2IsS9bUErNmuQ2CZPjWqo2Cwrce1PjD15c2nTz63HSBT16e5LueGSNjvvOfjWVoZE2NjKlj6uoa4eA8VOpZWz6otDwUBVodz0za0HqP3biFd+0QsRsLzXWln664KKZ1cqs2ThChq+q6WR+blSyuLzQIophnJgvcXGywssnPLpFIJJK9ZWDEx8xwloylUW575CydZ2dKXBjLsdxwiUl8Cy0/IKOr6JrGs1MF3lxuMV1M4YUxq22fsbzFqZEM96ptghgerDqM5kxOD6eZKWVZaXmcG8lTt5OFZm8vt0jpKilLQ1WS0e0tL0QBhEj+ZAwN01CZLqb59vMjFNIGuZSOqqqcHcmQtXTmqg6OHzKaS+GFEYaqcWOhgSKgmDGpOwGKgOtzDdKmzlguxXBHXEwW09wttzg9lKbc8vjy22XuVNpUOyWgctNndpOZHEMZg088N8mLz01yZiTT+3jK0MhaOllTQ9+wWXanMynODGcYy1m8udTC1DXqTsBK02O8kHqotRSSIWLdIWcCsW7mynghxVg+yYbsxpQ5lrcwNJWbiw0MTd2xkfO4Df2SSCSSo8bAiI+hjMFw1kRBYShrMJQxEEJwt5wsiHt6ssjdcotSNslgzNVdEIIYQcP1CcIAXVOYKaYYzaWYKiabbadLKZ6dKqBrGmdGMpwZTnN7pY2pKXhhRBDHuGHSYYNCMjysaLDUcAnjZOS7oqgIoOmFZC2dtKlza6mJ7YdcnMgxXUqRMjRGcsnOl7oT0nB8LEOnvGojOj/fYt1FU6GU1simTO6W2yzWHdKGynzN5fpCnesLTe5VbR5Uk+ffiKrA+y+McPXyJO87P9wTF5ahkTN1stbDgmMtO51JMV5IcXmmiKoonBvNYXtBr2vFDSI0lYeGiF1faCAQXJousFBz133vfhmO7URCd1Bcd15I9+/bcdyGfkkkEslRY2DER7nlY6galyZTVNoB5ZbPStPj2lydr9xexfEiRvMm33a6wHIz5P5qi9JQimrbp+FFGKrK7KrLWN5MfBZBTCmjM5y2uDxTZDSfwg0iHqzafOt+jbeW2zhBiO1HGLpO2gBT09EVge2HhJEgFIlZMmtqmFqW2ytNZistNEWl0k5KMm+vtHj+VIkPPzVGztJ57UGdVx9Uma+5uEHUK3fcXvEI4mR5W70dcGEiR97UqbkhigJvLrV45X6NtvdwtwokpZofemGa731ukuGsiRCCthchgoixnMVU8eFhX/3IWTqqAtfnG/hRxOnh9ENL6yARC2dHstSdRGy0/Qh71Wa1HaAqD++l6X59EAkWai6qAm4QcXultWn2Ybnh8sW3yrQ7ou7DT40yUUz3Pq+q6iN5POTQL4lEInk8BkZ8tPyI2dU2by0LTF2h5ScGzOWmlxhAhWCl5fHmso0XRlTskFJaoe4mQkE1FKpNH0NNzJxLTRcvilDVJu8+P8zZkSx/fnOFW8tNZqttaraLomgEUYgfBQShxpkRnZGswYOqg6qA0ekAUVW4W26TTRsoQDalM55NIVBIaSoLdYfrCw1GcyZemIxoF0DVSUaaq4qCpkAQCxpOSM32uFNpMzWc5fZKm6WGS58kB7qq8OR4jlOlFM9NF/jQU+NMldLkTJ22F3T2wfhcixu863SR0ZzVM2tuVmoYy1vMDKVZbnoYHVPvxiFg3YxE0w2YLqWw9KTLp9L2ewE9ZWhcGMs99L3XjkmfrzlEMZtmH+6t2twuJ6bWpWabsyOZdeLjUZFDvyQSieTxGJh3zayedGqkDAEoZPUkiEQi8Q+M5pP5FA3Hp5Sx8COXuZpDywuo2SGhEBgKCJGMaDd1jZypEYYxby81sTSVt5ZbeKHA1FTyaYsojrB9haxpoKjQdnxsBdpeQCTAjwSWQZINCULGi2liIAxibD8EVWHVgUDAfNXmz64vstL08aOYasvF9iJcPxnTPpzVCUOIEPihYKnpcnvV7ftaDGcMnpsu8MR4FjcQTOQthjMWpYzBTCkJzpW2R6XlJxt9mx4Nx2csnyKfMrYsNSiKQsrQGM1Zm2YG+mUkuntw+gX0jeWT86PZzth4ts8+CEHLDai2kzH0/bIwu0UO/XoY6YORSCS7YWDEx3zD537VIQgjDF1jru7x3CnBU2M55lZtQhGTtVQ0VaXcdhEiThbKiRjPC8hnLBw/YrnpE0YxdhDRdFSemcwTxYLZSpsgivCCiLYXMZY3SRsaQSjQVBUnCGj4EWEkaAcCtbM1FwGqquEGMTcWmxiawlPjOc6NZDg/lsPxQ+brLt+4t8qX3l7FDwIEKqoSE8egq2BoKi03pumFNDcpq6QNlclCiqfGs5wbzfKdT4zghYJr83WylkbGUqjZPssNl7F8Mm8jjAXljh/CCULaXsgzk4VNg/3aeRxNN2CuKtA19aHMQL+MxHvPDW8a0Pt5LHZS3jkznGGsYPHWUgtTV2k4Yc/U+jjIoV8PI30wEolkNwyM+Gg5yf6RXMqg5UXcXmryWilDPmXwzGSBO+Umrojxg5CaE7La9Ci3fMIwIhBqz4ugqQpDGYuFmgOKoNx0mV1tc2WmhK4qRCJmKGNQypiMZQ0Kpo5QBN+8X6PWDkBJulxQIGuq6EryNWlLx1RVdE3h3HCGc6N5zo9luTZX563lFq/P16l2JqE6XshkwSQSES1f4Ll+37KKQrJAbSht8MR4hiiC6WKaJ8fyjOQsri80MTWVuhOgKQq3lpp8fbbKE2NZnp8p9qaqmppGMa0DW5caugEojGMgEVjFdDKno3/G4Z1DbxXQ+3kszo9mNy3vrL2FzxTTqALOjeVx/FD6M/aJQfTByGyPRPLoDIz40DQVN4iStlTgTsVmZLHB5ekiThBSa4dkUjqrdtJ1EQlw/Ih82iCjC0xD5+JEnrurDgu1Nqqmkrd0QiFYqrt84IJGKWOSMTQ0XeX+qkPdCVht+9hewGo7wAsFWmf2VhRDGAsiBEMpEx2VfFrn9FAWOxAs1G1KWZ2m66OrKiqJr8P1wqQM0wz6DgGDpPPl0mQeVUlKLC0vwtRUsmmDyVIaUHh7pc2dik0pbTJfTwahWYbGzcUm9yo2TTfkQ0+O8r3PTfYd0NWv1NANQDOlDG/M11lueggU6k6D5zviApKMxBNjSVvsE7l3JpVuRj+PxVblnbW38JYXkk0ZuEHUNwsj2RsG0Qcjsz0SyaNz8t8hOuQsLTFlRhGKUHlQcygtNVmuu9yv2Sy3fZS2R932CWLBUMak6YbYboCS0snrGlOlNFHnFl9tezS9CMtQqbQDPn9zBU1Lbj2u3+lCMVRuLrrUbB87iPEFaCGogK5D1tQIo5hCSsdQFXKmThiFVG0PgWC17bHU8Li/2sYOIvwIunoj3iA8FMDSFdKmypmhFIoS86DqsdLyyJoG50czTA1lKTeTaaKlbFc8CAw92dJbq7vkUzpjOYuWF9L2Iy6M5XZ8g10bgMI4yZj0uwmPF1J8+KmxdaJmq66V7ZbjbQx4LS8kjGLSps5i3WaqmOaJ8Sz5lCH9GfvEIPpgBjHbI5HsFQMjPvwo8UboioodRFRaPoqi4ked0eaawkLTx/NjFGC17RPHMWbK5MxQGkPXuF+1qTkBQRTiBBG2FxDHOkEYU217xEKh6QaEsaCQMihmNFw/xPVjohgMBUwdcqaOH0Y4foShKjS8gFTH9/GgFhMJaLgRbhBStQNqTrhpliOlq6R1hVzaQEUgUDBUhXLDo+76DKsmURyhIMhbOmlD491nSgxnDJpuUoa4Ml1kopDi2lydpYZPGMfkLH1Xt9duaaVbZsmYKnfLba7PNxjKGuu+19oSy8Zppt3bY7+U9sZb5VaipOWFvNrZvJtLGeRThryV7iOD6IMZxGyPRLJXDMxvy2QhRSGVBNyMpZHteCeG0xaQbIv1ghARxxiGhqlDSjfRFMimDBwv5O3lFrGAphfR8kNQVYI4xgsFy81khLofx+QMFS8MURWdlKkTOyERSdZCF0lHSiAEiqoSxIJ6O8DV48QrISASgsW6R9DPyAGYmoKhJWUYRFIu6QZcXVNwQ0HNjQhDwartE0WCrGXx3nND627/3exDd6vt0xP5vgvn1gqBjJHMICm3/N5gLlVVWWl6vDbXWLe63tQ1gjhmupSIibXZDUiEx1duV7i/anN5poTb2T+zsXSyWUp7s4CXTDvN0PZDzo1ke3ttdhIYZR1fslMGMdsjkewVAyM+nj9V5MmJPOWmSySUXonCsnQadkS57dN2k3X1dSfE0FRGsip+DHfLNiCo2T5+lLSy+qFAU0E1FFRVQVMVbD/EDWKKqTRVx8cN2vhhjKUqGJogCKGYNtBUUIRC1jKoOT5tL8L2QxKb5ubkLY3xvImpCubrAYoiiBVI6wpPjWfJGhpjhRTLDYev3vZRhI4TRmiaQtPx8MKYC2uC6cbAPVFM952DsVYIzFVtHtQcTE3F0BRWO7M5Ki2PMIqZGcr0Vte/58ww8zUH2496wqQrJAC+dKvMq3M1lhs+y02PcyNZRnImOUun4fistnwKaZ3VVkDTDXacuVg/wCxet+tlO2QdX7JTBjHbI5HsFQMjPoQQaIogbapomsZQKln3fq/cRlGS9fVNJyCMY2IBfhjT8iKGcybDORMvCHEjHdf2CaJk/LeuAkIhFskI9XzKIGMKarZLww6xdQ1dVVA1BSUGK6Vj6Rq6qlJMw0ozER6h2Fp0aMB4wex10ay2fLzQRZB8XZRSGcqYvPfcMHUnxI8EU0NZHD+k6vhMFFL4QuEb92oPDfza7LXq3v67y+jmqjbnRnNU2x62H3Ll/CivzK7y9TsVLk2XaHkBQtDbFNz0Al65t0rGSDYE3686PDmew9TV3nbdlhcyXcxQsAzaXoAXRVTaPnUnJGWo3K/aBOVk4+zlU4Vd/Xs/6q1U1vElEolk/xkY8fGlW6tcm2/S8gW27+KHJpauEaHgRzFNN0BVQVNVYhEnu1bcEMvQyJsRVcdnteXiheBGHdOoItDUCF1X8IKYMAwYy5m4fjLEwwsjQgXiGDQ1CeqOH4CiUHUUVu1w0/OqQLzm/6+3fVRVSWaM+BG6Boam4YURuiK4W27TdHxUVSdjqgznDDK6xVJDxzR0hrNJCWknwXTt7b/pBjS9gJWmz1LTw9JUMqbOzcUGADnLZLqUZq4mGMmajOQsHD/k9blkS+zt5RY128cJY26ttPmOc0N829lhIDHc3l5JskPDWYOhtMlMKZMYVqOYU0NpihmDuh1g6Zvvk+nHo95KZR1fIpFI9p+BeWettpObfBTFhGFMw/F55d4quqIQxjG6ppCxDPwgJBaJWIiipIPEDnxqdoDtJ9NGIREGoQBVgCKSTa/VtocfJuZQL4SQ5AVWVYgjcMOktLKJlQNDhbGciROEeKHACZIx6kJNvlfD9rDyacJYkDZ1bC9CoODFgns1Fz8U6HqIisJoPsmELDY9FmoudSfgzHBmR8F07e3/lVkHFXjf+WHuVto8M54liGGuk+EwO4FaV1XOjmQZL6S4vdICFFKmloxR9wK+4/woc1Wb4azZy0Jcmiqw0vKIYkHG0LD9kFfurZKzdE4N5QljiGLBSM4inzIe9z+BHSHr+BKJRLL/DIz4yFgarh/RcEJUBTJW0t4qBLTdEFVRyFsqkW6wavsEEWga2F5A21WJomRo1tr6SAyggOsLWr5HDDhhIhi6FsWw98D+KCSiYyJvMl3KMl0weW2+TqXt4YeJwInj5CwxCrYXEouYnGUiOiompatoKuRSKl4I06UUFzoljmJK5+yFYaq2z9mRzKbBVAjBcsPl3qpN1fZpuiFzVUHW0lEUcIOYmVKGQsZivuYylLHQ1GS8+doFcJBkD/woYqXpMT2U5nY5Zq5qM5ZP8dREvuc5SZs6F0bzTJfSvD5fY7Xlk1VVhICRrMlYPnXgIkDW8SUSiWT/GRjxkdZVihmDKI5x/BglFgxlTW4tNWn5MQqCWIAiYrwoGQJmqMkW1VAEtHzoN7g8iNZ/XGz4334oJHNH8pZGLqXjBskQMCcIuVuNyKdMhKIShA6hEEQd8WJqCqqi4IQxvh2gq2oifkLBdN7E0HUMXaGUNdFVhUrL517NJlhq8uRYjoypcXulhRfGWPo7Jsy2H+EGEdce1Hh9oYkfRkyVUrz//AjvPlPqPSZn6TTdYJ0nYrMFcO85O4SiKIlAKabQNZXJYorhjEEcx5RbPpWWR8sLmKslP+NoLsWl6WR8ux3EXBjLSBEgkUgkJ5CBER81N9mEiqKgaoCqsdRwWWp4+EEIqElnhJ4ID0uHMAQniBEiyTyoAkTcyWZ06L9JpT+aAuM5k5mhNK4fsWr7VFo+KUNFNxVGcxblpkPDTXaQBHHyNSlTBSHQVRUhYnKmQSgEiqIylNYZypp81zNjeH7EqhOgK1BueRRTOkNpk8W6S7nl8ZW3KzhBRKUVMD2UIoxigjimmDKwg4h628f2IsI45s5Km7PDWc6N5h5qN93OE6EoCpemCox2hpW5QcRc1SEWcG2+yVTb58Zik6abmFRPD6U5M5xhrursaLHcXrW/yrZaiUQiORwGRnxoiKR8oUBK1xjL6ozmUlxfbOHHEESJyVQJk6xFEL6TvdB0hTgUGBqkLY2WHxHGSUlkJyhA3lI4PZTmyswQC/U25VZAGEUIoSDimIyhsep43Kt5tN2QSHRKNypkdY0nxrKMZC0WGzZeEOGG0PIDUobG6aEMxbTJfcem3g7ImyZLDYeVhodlaEwUUiw1HBbrHqM5izvlFi0/pNJ0qTo+lyYLNL2QKIyouhGmCnYY8+W3y8zXHT7y1BjPTiftsd1BYpCIhjiO+dqdCpDMBhkvpFAUZV354vZKMh+lmy25tdzi7ZU2pbRBzQkeEis7WSy32SAyRVF2LCr6bdft12p8EpHCSyKRHCb7Lj7+1b/6V3zmM5/hp3/6p/mt3/qt/X66TQmFgqIqiEhBILBMg7SukTVVwkglimIikqyGgN7MDdPQaDpRYvwUkDESw+h2wmOtPUQADU9QcxNDZdOLWG37aKpCxtQwNZ1YKMyuNGl7ggDIGgphJBjOGpwaypI2NFAE50ayrNrJXIwo1iikdKaKKYYyBvdXFZwg4nalSbXlk0/p2PWQhYZOFCXj2UtpAyeImK200RSFcjvgxmITN4iZKFogBLaf7ERZaflUnRARw1g+ac999UGdajvAjyK8MGax7nC7nAwmuzCa5SNPjz3UyruxgySlq7S9kCgSuGHUWzq3m8Vy3UFk37pf653nPWeHEhPrDmd19NuuOyjiQ84zkUgkh8m+io+vfe1r/Pt//+95/vnn9/NpdsRozuTscBpVVai1fJ4ZzzIznOHGcoNKO+g9risYuh5R14uISNpdgxhW7J0VWvppk1o7oKEEKEIQRRBGAiEiTK3blquQSavUnQg3EKQMldPDGZ4az3Kv6tByQxpxjKooZHQNXVHIGDpBFOMGMaeGUuiawqv3qzhBzFBGpenGNFyXS9NFluouAsGlqTw120dVVFbbLn4YoSgKOVNjOGNRSOu8Pl/HD5IZGw03GfKlKArVdrf11qPc8tA1hVLaABTaXv+tsRs7SJYbDpqiUHd8MqZO1tK3vIlvtcNl7XkURellT3Y3q2OHKawThJxnIpFIDpN9Ex+tVosf+7Ef4z/8h//Ar/7qr+7X0+yYpycLXJwqUm65pHSNQiZFzQk5M5xmvuqgqhG2J3qDu7qZC7ejQrZoWNkxdiDQVdDVJPuiqqAiODeSYyJvMF+1ieKYlAqWoXBqKM3FyRxhJFBQEMB83cMJIrxQEMUx8zWXmudj6skSt+limrYb8vZKCy+O0dRkr42uwJmRDO86M8ST4znemG/w9kqLcjuNIgSFjImhqWTMZIFete0zX7MJnWRr70Ld5anxXK+LZTRvIWJBKOJkcZ4fM1EwcYN3MhmbkTI0npnM92Z4pAxty5v4Vjtc1p5HV5XeY3Yyq2O323VPEnKeiUQiOUz27R3nn/7Tf8r3fd/38fGPf/xIiI+Lk3k+9swYX3pzmbIaoCoxD2ouhbSJaWi4YUxKT+ZzROzPXVgAKUPFC2IUBYopHV1TCcKQm8s+kUj2vxiawjOTBb7j/CiOH+JHUS9QmJqKZaq03YgoVnH9ZIdLue1xcSrPcDYpnSw3E8Fgd7pU2l7E82fyvP/CSM+X4QYRlqax0nQZzacYyeoM51PkTI1SSuftZQs3EsRRMsTsqfFcr4tFVxWGs0ZnwJjD2ytthtIG8zXnoSmqG4XFdCnFSM5aN8Njq5v4Vjtc1p6nO5p9p7M61m7XHbSZHnKeiUQiOUz2RXz8j//xP3jllVf42te+tu1jPc/D87ze3xuNxn4ciUo7YKnhUXUj7lZs3lpq4YUREwWLjKERhQHVcHfdKztBV96Z+WHqgEi8I5qazOfwY8H9mkvTC4hFMvVT1ZTET9FwCKLOsrlYkDVVTF2j5Qa9QWVDWZNC2iSKkyCd7DOJKGUSYTBftbl8qoSlqTw7XWQsb7HS9Fhpeli6zt+6VOLmYouJosVY3mK+5uBHoGkqxYyJ4oaMDlsYmkrbj7g4mQcSQdFdLJc2dYRQNk3hbxQWlq72DXy7vYlvZlTd6ayOQZ7pMcg/u0QiOXz2XHzcv3+fn/7pn+aP//iPSaW2N7B99rOf5Vd+5Vf2+hgP0fJCmo5PGMWEYYQXRBi6QiwEFTtgtR3vebZDAdKGStbUCUUMIqbuJGIijMAJIwytM8CMZEOuG8VcGMoyWUgjYoGuKuiail1zO2ZIgakpWGaM5wcMZ1MMZ3Umixa2F+KHcHY0x1LLJ2OqnB3NU0pbDOdMzo5kKbd8Xn1Qp9LyeFB1ABjOmVyaKnREAr1x6U+O51hp+UmWI2fgBhF/M1vl3qpN1tKZr7mM5qwtU/hCCNwgotzyqNk+Izmzt95+beDb7U18o0fk/Gh2z7s1HqUjRHaRSCQSyfYoQog9jbl/8Ad/wN/+238bTdN6H4uixNCoqiqe5637XL/Mx+nTp6nX6xQKu1smthXLDZf/9pVZPndtgZWmixfGKEpym98vFJLMh66CqatkLT3pclFU/DhGBXKWihsmy+wURSFraJwZSaNrGpCIo6ypUXVDzg6n8fwY01CIhYJKzGQpw1NjeVQVwlhwf9VGU1RsL+DpyTzPThdIGRp+JLB0ldW2T6Wzifb6fIPJYopLU4VeRuTVB3XCOKbthZweSpNLGVi6ihtEvDHf4F7FpuEFfOyZcbxQ8NREjvOj2U0D7nLD7duR8rgBebnh7nu3xmbPsZXAOIhzSSQSyVGk0WhQLBZ3FL/3PPPx3d/93bz22mvrPvbjP/7jXLx4kZ/7uZ9bJzwALMvCsva/3jyaM2k4PosNj6YbdbIc+yc8IPF4GFq3zKIwmjOoOwF2EKPRaeftzPNQFYWUoZJLJf4MQwddVXHDZIlcydJYWLVxIzA1gRcpXJzIkTZ1IhEjYpVLUwXm6y53VlqU0ib3qw6XT5UopM11i+IUBRZqLiM5i0tThYeMnbOVNi03ZLUd0HAjnj9VZLXtc6dioysKy02fa/N1Lk4WyVl6L4U/1gnKd8rtXlBuecmunO7k0pSh7Ukm4HG7NXaSodiqxXczgSG7SCQSiWR79lx85PN5Ll++vO5j2WyWkZGRhz5+UPhhzA/+v77EjcVm38+ryubL3h4XL0wyH6CwVPcIOrtfQhLRUXMTIaKoAkOLafsx1ZaLZVnkLZXxfIqxnMVK28cJIgKhMJ6zWLUDlhsOc3WX6UKKQkan7gbU2h6GqnJhLMs37lX50lsrvOt0iTBOdrPMVQUjuWT7bMZQWWm6XF9o9Pwb44Vkn8pqO2CqmOLGQpPrCw28ThdLNqUznrc4PZTh+VPFbYeBPWpXxVpxkDUTwdod8T6W37rUsxN2MudiqxbfzQSG7CKRSCSS7RmId0ZTV5NAukZ8KAqcHUozU0px7UGVur8/zx0BSpwsZnNFUl7ReKejptfWG4MTCMI4QFUUam0PP9QYz1tMFS2EIiikdG4utJit2qQMnSASNNwAQ4VVW8ULI0azaeaqdf7o+hJ+EJPSdXQ12WszX3PQNZUzwxkUReEb91b5ws0VhBBkTIP/x7fN8NxMqRdAbyw0uV+1EQi0zsZdO4iYLFo8Of7w2PV+Qfn8aPaRuirWioNutiZnGT2h8LjdGjvJUGzV4ruZwJBdJBKJRLI9ByI+vvCFLxzE02zJ3/22U3z+5gqFlM50McWpUporMwW+fq+KZRoofrBvo6YU9f/f3r3GRnqehf//PudnzuOZsb32eu095LTJJts2J9K0hR/tryiKKvpHKgUFKSW83IiECEQLQgGhNi0SCNRWoQWUvoCoVEBaqFRKaCH55y9C06RpkzbNaZM92Ls+j+f8HO//i8ee2rverHd37Nm1r4/kF+t4Pfezu5n78nVf93WBpqlu59T1eoZoJEcwfqQwSAIm09BpBjELrYCOH7PQ9MmnTQwN2n7EbD3CXr52G6gAXVfcOlHCDyOmai0GMykGcw6GlvS0KC8Xhyql+OGJKv/10xl+eKLGNcMZFlshb8w0uGF3sbuBvnKqljQlWz4yyacsppc6eIHihWOL3dsm79QM7GJvVawODl441gYNrhnO/yxQyLvn/L4bOVLZSIbina74SoAhhBAXb0dkPgDGBlL8P+8aYbLapuNHlLI2accgDGOCsPc3XVYL4yT4MEk6pa5kO1aCkIgkG5K2dUxdx48idJI5NKau0/ZCLF0niCNM3SRtQdY1mat7eH4EpqKcd4lijeeOLXLVYI7hostszeN0zWM4b5NxTPaW08w1kqFux+ZahJGiHUUcW2xRSttJC3d+tulCMtX3VLWTZE9SJtVmiB8FTFY76Mera3p6vNOmfL5jlHcKDjKOiaax4aOMjRypXEoA8U4BlbQtF0KI89sxwUe1FZBxTEaLKV49VWO+0aGStTF0DbXJNyEVEEWgG0mvD7U8GyaOwQZiDdKWRs41SdkG9Y5GJ1BEscILQ6ptDdMwcE0Tx9RJWQbtICJl6Zi6hqXrDGZt9gykMQ2N3UWXYtrmuN1iruExkLaZqibXaqeqHeYbHm/PJ8Perh7MEMYxB0fy3Lg7z/RSm+MLyayWZBBevhskKKV49XT9rI6i52sGBms35YaXTLPNudaGgoP1gpV3spEjlc3qcyEFp0IIcX47JvgYzDloaMzWPUoZl73lHK6lE8YKYnBN6ISb9/oRoMdgmRq+Ut20h6aDpSWb4UAm6cURxorp5Z/4XVNnsppcDR4ppsnYOqAxXfeoeyEjOZdSzqKUsRktpjF0jYYfgRbiR4pyxu0em8zWPcI4Zjjv8FbKxLV1rhvJkbYN3jNRQtd1/t/X5zg61wTgwGCG9189yP7BLJBkL9brKLoRa45RjrdBwbW78hsODlZnToB37J/Rz6JPKTgVQlzOLpdeRDvmnfG6XTl+6dAu/vuVaZa8EMsE0JKrnzpo0dpJtJshUKCHyd1aTQfi5EjGMnXKWYdrhnIM5Rx+OFmjEyoMU2EZGpap044Us/UOumaTsi0GUjYtPyKIIjJWilsnSly9K898w+v28Vhsecw12jz1WjLI7dDuAo1OwI/mWmhojORT7CmlGcjYlDM2DS+k6YUUUzaQTLY9M7NxcCRPOWN3syNKqfPOcoG1m3KSRdn4MQpc2HFGP2sypB5ECHE5u1yOhndM8KHrOndeVeHqoSzHF1ostnxeP12n6JrkXYuOn9RZbG7nD/BWrriQ1H+kzCQbkrYNsq6JZRg4JkxUMnQ8n6xroGkO4GHoGteP5JlrBNQ6AbFKqkdcy2T3QIq0pfP92Savz9Y5sdCimEpuxEzXOkmAk7EBDQO4aleeth8y2/BRaCy1a4wWXTKOyXR9OfORzZwVGGia1m3jHsWKpXaNm1bViKx2Zp3HyhHOhR6jwIUdZ/Szdbi0LRdCXM4ul6PhHRN8QLIxDBdSDBdSHJ1tcGy2xVzLp9ryCKLeTK7dqJXrtmgaWdfk0GiBMFa8MVOn4yssPcY0LPwwZqraJogUBdcin7LoBEm30qGcy/sOVNhdStHyI7718mn+960FOkHEfN3n5/aXKaQsUrYFKGYaSQATKcUPjlexdMVQIYVjuhxbaJF3Dd53VZmJcjLddbyUZjDnnJWmq3eCDf3jXS/CXjnCuVBynCGEEJfucnkv3bHv4FnHJIgj5hsekdLQDIXapLTHSk9XS0uai60c7RQdg8GcTSWXohUELC5FtPyIKI5p+klB6VAumbo7WrQJQsWppQ4TpRwHRwu8cqpOJWdTySZTaheaPmnbZKSQou2HOJaOrlvdGo6cYzCQtsk6Fv97dI6BlM3UYouTiy0yjkXGNtlbyXLrvnI34HhrrkkniJhcbCc9Span0m7kH+9KhL26WRm8c73GuchxhhBCXLrL5b10xwYfgzmHq4ZzVLIuS+2QxZa/JjDoJR1I2ckUW8swSDsGQQS7Cg6FlE055+CaBoNZjROLTRabAUN5F9cyuGY4w3R9hoWWz56BDKWMg2vr5FMmE+UUxbTNaNGllE6KTt+YbdL0Q3YXUlw1lKWSdbqZjLRt8Mqp2vL8FihlHbwowtJ0btlXpu3/rMZjddZirpF0TV0pXD3XVNozrdesLIjURZ0xynGGEEJcusvlvXTHBB/rVfgeHity4tpBDF3j9Zk6s3UffxPOXgwd0o5J3rUZyNhkXQPbSIpM867NRDnF6VqHU9UOjmWyp2QyNpAhXi7k3FV0aXoRrm0wNpDiht1Fml5I04twTIOpaodyxuauQ7vYXUzR8kPKWQfH1NE0jVv2ltA0DaUULT/i9FKHwZxDx48opi0qWYfppQ5+FDEepFEq6Sq60PDJp0xaXohr6d1Mx+qptO9UOb1es7JT1Y5cPxVCiB1uxwQf56rwvevQLmKlWGp7LDQ2p8d6J4bFVogXJHmVtq9zeGyAkYJLyjFRaFSbEQNph/GSwY1jRXblHabrPicWmhwaLXL1UJZjCy32VrIcHMnz1lyThWbAaDHF5GKL4wstylmHd40PoJTipckab862MPR291k1TWOinGGpHTDf8Aljxbv2FAB48cQSlpEEGJWsgxfGnFhsEczFWIbGwdEyo8XUWZmOd6qcXq9ZmdRrCCGE2DG7wLoVvnmXN+daPP3aHCcX2nQ28aqLF0GsIsKlNoW0w55SGscyQINi2saxdA7vKaJpGrsH0mQdk2MLHQzNoNb2mK557C6mmShn0DRtTdFQvRMwVW3R9mN0HfZXMiiS73NmQWiSjSiuyVS8NdekknXW/Nk4ps7YQIpC2mKplQyZW69Y9FJmpAghhNiZdkzwkbEN6p2AF44lzbtWrnv+9FSNU7UOhq6jNvGi7crslhiNThDy8lSVw2MD3c3dMnRq7ZDScuOulU392pEs1bZHre0zkLaI4xilFIM5hxt35zm+0GK61uanp+rEJMFAvRNQybpM1zprnhXWP+9bCWQmqy2aXsh8wyPjmJSyFguNgDBWeGHyusCaY5aMbVz0jBQhhBA7044JPpRS1L2kjiFWScOuph+haxooqHWCTXttjeTGi64nRadpy2ChGTDX6DCUT4a97R5IMZyzCWKotX1O1zxm6x2OLTR5c7bBUjPkx1N1TlZb3H3jKMOFFADHF1q8PZdMui1nHLKuSaQUXhSRNpKZKOezkpk4Nt+k0QmZb/hUWwEp2ySIPGzDYHIxOY4B1hyz3Lg7f96sxuXSUa+XzvVM2/FZhRCi13ZM8HFisc1s3aeYsnh7oUnbjzgwlCPrmOwpp5istjbldR0dbANs06SYMhjIpii4JinbZKrq0fAWObS7QDnrEMQ/m71yYrFF0bWZWWpxfK5FK4gwjWQs3Y27iwwXUhxfaPHmbJOMbWPqGn4UMWg7FFybgbTNSCHF2/NNji+0ujUf61nJTDS8sFtHMlVtE8WKwZy75kgFWHPM0vQj9g9m3zGrsbouRNdg90AK1zIuaXPu9yZ/rlqXy6V7oBBCXM52TPDxMxpBGBOrZAONoogB16KQstC9gGaPa04tA3Ipi8Gsy3glQyltstQOqbZCimmTVhiRT5lEserOXlEk11vHBlKYDYNWENIOFVoY0Q7ss14j65ocGMxy1WCGg6OF7pXa/31rAYCM3WKinDnnJriykc83PBpewGRVYeo6gzmHqWrnrCOVC21Q0/BCwjgmZRm8NFnljdk6+ypZTF3f0Oa8XqDR703+XLUul0v3QCGEuJztmOBjvJRmfyVD0wu5ejnjMbnY4vWZBscX2zS8iHaPA4+UDoWUTdY1qeQc2n5IZiDFnoEML5yo0gpCYi/i5GKbfZUsgzmHV07VeOVUjaV2yCun6mRsnfFShroX0vJDDgxmGS/9rAPpyjPdNFbk/VdXGC6kuldqm17E3kp2Tf+O9axs5GEUoxSUlwfcVbI2layzZtNXSjFaTH7CH8w5VLJnB0NnyjomTS/kRyeXWGz62KbG9SMFOkG8oc15vUCj35v8uboEXi7dA4UQ4nK2Y94Zh/IuH7hmcM2I9uMLLeqdgKYXAKqn7dUdHQayNq5toGsa842AXMqgE4SYhkPaNtCUzmInJI4iRosu1w5naXohjXbAe8ZLLDY9Rgou+wazTC8l11QP7U42Xq2W9OpYeabV9RY/u1Ib0lk+rkmGua1/VLGyka/cjilnnW4W4cxC0dm6x1S1QxQrpqodKqu+9lwGcw7jpTSNTsi1wzlePV3j7fkmu4vpDWdOzgw0+r3Jn+sGj9zsEUKI89sxwcdqmqYxmHNo+hHD+RRoGn6oejLVVlv+yDoGhg4Z28Q2dGYbPk0fdhVSvD3fZr4Z4FoG842AyarHD45XgSSbsTK0bayU4cbd+W6A0PaTbMjR2SYZx+xmOtb7iX+9TfBcRxUXspFfTMZhdTAURjH7B7NMlJNrwxvZnNdbX783+XPd4JGbPUIIcX47JvhYb+PN2AaoGFNPbrxcSuBhkPx+AzDN5FbLrnwquX0Swa6ChmsaXDWY4ehsgyCMyNgarqWhaXBioYVSiv97/fBZm6qmaQwqxZM/Ps0LxxYoL1+jnSinu7dezrSyCQ6umtEy3/AIo/is/h8XspGfGQhkbIOZWue8hZ/rvcZGC0TP9XtlkxdCiCvTjgk+1vuJPWMbVDshrmlQylosNgOiOBn+dqEikoyHroFlaBQyNsN5C0NPrrv6oYVtKF6erDHT8PCCpKdIyjKIlvt22IbRvT2yOmhYOTJ5c7bBfCvAjyFj6xta1+qgq+EFKMVZGY4L2cjPDASUUhsq/LyUYEECDSGE2F52TPCxXuq+4YWkLZNi2qbWDun4EV4UE4dcVP2HAjwFttJYaoccne9gahpoOkM5k9GBLKeqHTK2QccP8ZZnqziGhoqhmDa7AcGZmZpCyqSUcTi4K8fpWoddBbdbeLpmDWfUddQ7QTfoOrkYY2gajqVvuFj0TGcGAkdnG3K7QwghxAXZMcHHuY4WhvIucRQz1/QIY4UCTA38SziDSdtJP475WtJK3Y9DBtImKI2MbbLY8plvBgwv3yTZM5Ah5RiMldLddXXH0RddfjK5xFQ1ptEJyNgmN4zkuXlvicGcw0ytQ70T4IUxjqnjhfFyj47kSuxo0e0GXU0vIumppnWH0a3Uk1xsr4x+F34KIYS48uyYnWK91H1yW6TC88fmME5pFNM2Cy0fpV1aAUjHj3FsCGNoh0kOZSBjM1RwsU2NU0stXFPHMjWUgolKmoG0g2sZ3c1/ZVN/ZarGa9NJdgFNsavgcvPeCgdH8t3syELD58Rii7GBFEEUYxk6148WmKq2cUy9G3TNNzzmm343S3FsvsnxhTZNL1xTwHoh+l34KYQQ4sqzY4KP9WialtwWybo4poFr6UQNhXcJd241wLU0XNuk3vJZanaoZFOMFVwGUhauqRMpxbUjipmah2UktRsNL2C+4XU38NXj6OfqHqaho2ngWHo3SFnJjuRTJsFcTCFtUWuFBHG8nIkAL4zRVs1hWWqH3SxFtRVwdK5JMWUzXW++YwHrO/0ZSj2GEEKIC7Gjgw9IaiQqOZsYODHfuqjAY+V6rQ4MZC1GCy7HF9qEaFimST5l4VoGDS/EtXRswyCfsdhbzrK3ksE2NI4vtJlv+Cy1w27R5krh5mzd4+hcE4AD2cxZDa0WGslguqVWQCljd9uXd4KIycU2sWLdOSxvzzVW/hQu9Y9RCCGE2LAdH3zM1j1q7YCMpTN7EaNBdCBtQjFjY+ka+ZRFO4jwwhDLNBguOMSa4tXpOqWcw/+5Zghd09lVcDk4kmcw53B0tsHbc20AFho+9U7QDTwGcw7vv7rCRPlnXU3PbGhV7wQcGsvjmDo51+rWbhydbRArzjmHRSnFgcGkSPRANrNuAet6+j1XRQghxJVtxwcfDS+k4cWkbBNjg/unTnIbRie5WqvrGq5lMJx3STsGC3WfrGvR9CNmah7FtE3GsZip+bw8tcR1uwocHMl3AwwvjDmx2CKYS+o1Do3lu6+1cjS03nFI98jjHB1Gz1cMOpR3ef/VZ3dIPZ9+z1URQghxZdvxwUfWMSmkLWxTp5S2mGkEBOscvTgGWDr4Ed3/rgMo0DRFa3l+ymDO4WinSRgpMraJricNx0ppE8cy2DOQ5qaxwpqN3jF1xgZSFNIWS60Ax9xYD4/V1stGnK8Y9GLrNfo9V+VyI5kgIYS4MDs++Fg51piudbANsMwOUwsdVs+Yc3QwNFjuC4apJZkPDdB0UOjoukHLj5hv+lSyDo6h0w5ibFMj71poWlJz8XP7y2dlCXKuRTnrEMWKctYh51oX/BznykZsRjGoXK9dSzJBQghxYXb2rsHKnBeXG0YL2EYy46XW9qm1Y2LA1iHraJimRRQGBEqjnLZYaofJT7c6RJHCNXXKGZtSxibvmHz/+CILLR/X1Mm7FhPlLB+4ZnDdo41eXFfdymyEXK9dSzJBQghxYXZ88AHQ9CPyKZt9lSzPvDGLqeukHTCJqeRShLGi4Ue4joMWRliWQckwSdsGoVJ4QYhrGbi2yUDapuBa2IZGOeNQTJsMZByG8uef/qqUYq7hUe8EawpHNyJjG9Q7AS8ca5NZvlZ7IS7k6ECu164lmSAhhLgw8i5JsnnoGjz/9iIzNZ+2HxIBXgSq5RPHMZ1AYWoBGcfC0nUWOj6GBsMFF9twKGUcDo8XOVXtMFVtYxkGtgleqMi55jkDD6UUr5yq8cKxRdp+xFI7YLyUoZS1Lzh9ry3f+b2YcgM5Orh4kgkSQogLI8EHyeaxeyCFpilSjoEfxbS95NjFayWFHqYOKoZWEC0PhlNoeoRe9xgrpUk5Jg0/ZqHpE6MxUnCptyMqeYtfftdurtuVW/e1Z+sePzhe5eRiG12HejsknzKXB8GF3QFz58tINP2IrGNxzXC+e632QsjRwcWTTJAQQlwYCT6WNToBjp1cl52ve92rtCsXX1ScZBSiOKbRjglj6AQhnmty9XCWnG3S8QPKGYesa3Bioc3+QYtb95UZKbjMNfx1A4eGF2Isdy59a7aBqSfNwso5h4xtdLMitmEwkLE4vKfIUN4965gkYxuXlPqXowMhhBBbRXYYkuzDSyeXOLnYJghjUpaBF0WoVY0/bQNSto5jGTQ6EVqchCW6pnF0rokfKcZLaRabPoXAJOuajBZdXp+u8+ZMnaxr8b6rzp6dknVMTEOn2kqOdEaKLvsG0+ytZFFKdbMiqwfODXH2McmZ3UsvNPUvRwdCCCG2igQfQL0TMFv3QGlEMViGRtbWaHoKw4C0AWnXIu8apCyTMGqjoWOYGnnXwNI1IqXww5hjCy0Gsxa6pvP2XINaJ+TwniJH51qYusYdByprMiCDOYeJcpqmH7K3nKEdRFRyyRXZo7MNTF2jknOYrXs4pt7NSJx5THJm99ILJUcHQgghtooEHyQdRmfqHnMNj2o7IIyTNummEWGbBhoKXTcoZVwqWYe0Y1Ft+TS9iIGMy95KGpTG5FIHL1DU2xGzzTamBvPLTcNsy+DEYovMyaVuMefK0QkkGZB2EGHq+prZLeWsDUDKMnj3eLGbkdguxyTSoEsIIXaeK3PH6jHH1Ll2JEsrCKlP+cRKUfdCbN1IpsmicEydjGPSDmPGSylunhjgtdMNdhddRgopJpfazNTb5FydmXqHpU5IJWuDrjHX9Lh2OM+h0QJeqKh3AoDlkfYt0raBUlDO2EyUMwzmHJRSKKUopCwKKYvxUpqhvLsmY7Idjknklo0QQuw8Enyw3GE045BLWaRsg3onxNENlKbR8UM0NBpehGsZGLpOFMYsdUJcW+fweIm2H+K2OnhhzMnFFnEMYRyz2Ao4UE4zXs4wOpCiE8aYuo4Xxrx1conJxRbTdY/b95XQNZ1y1ulmRF45VeMHx6uYukY5a6Np2pqMwHY5JpFbNkIIsfNI8MFK3UWGRicgjhQdf57RgQzTSy2iGAopi5OLLeptD9M0GcraLDR8iimb548tEEYxS+2AxWZAy1egYoYLLjoag7kUVw9l2TeYxTF1NE2j0QkIo5i9lSzTdY+355vsLqa7RyezdY8Xji1ycrFN5YxC0+1muxwfCSGE2Dh5pyfJIkyUMxybb6HrGrmURcsPcSyDpU7IYquDbhgU0jZhrNH0Q1K2ybW78pxYbNH2Q47Pt1jyAkoZk4VWiGPq7C5lKKZNXMvi9JKHrkPWsWh4Qfcmzf5KholyunvcAkmgYRsGg8uFpinL6G7K261GYrscHwkhhNg4CT6Wrdw6aXQC9lUy/GSyRr3jJzNcDBOdENc2Gc6lqLY8YqWYqbeBmAOVLEpB/VQAWjL75cBgngNDGUoZh4OjeV44tgAaXDOcZ7KqKGdsylln3QAi65gMZJLhco6p8+7xIpWszUyt060TyTgmpq5f8TUS2+X4SAghxMbt+OBjdSYh45iMldJMLrYZr6SptU1OLnaoZG0ans5gxmZfJc3xeUUYa7SDCA2N6UYHLwwZK2UopUzuuKrC7ftKBDFMLraZqibzVjQNpqptTF1nopw5Z9AwmHM4vKe4JhuwUpi5uk6kE8Tb9jhGCCHE9rXjg4/Vty10DXYPpCikLOIpxWzNwzI1Zho+A2mT0YE0+yoZbCO5BaOUotr2abYDHMvi0O4ssVJcuyvPVcN5ppfanFxs0QkirtuVpZJ1aAXxeY8X1ssGrBRmnqtORAghhLhS7Pid68zbFq5lcHAkD4CmIO+avHhikV2FNLV2wGzDoxWETM60sUydnGOSTpn4NY9qy8dYDkpm6x7/35vzvDnbBCCIFAdHNFp+xHzDQym15urs+awUZrb9sFsnMl5Ko5Ti6GxjW9R/CCGE2Bl2fPBxrtsWmeW255apk3MtvChkZqbDG9MNXFvDNExGCjYjxRSVjM3r000Wmj67CikyjknDC2l4IcWUBWicXmozW+9Q95KBb/srGd5/dSW5/bKB4tH1CjOlR4YQQogr0Y4PPs61qU9V21iGTj5lMpR3ObXUIQZOLrYoZxwKGZ0g1Gh0QuYbHgXX5PrRArmUibt8OyXrmEzXksxHzjWJ4rgbjDS9kOMLLZba4YaCh3c6ipEeGUIIIa4kOz74WNnUV0bXvzXXZL7hEUaK60cLTFat5eyIznyjw3TNxLVNau2ArKMzqlLM1Dwsw2Cx5VPK2uRci8Gcw/uuqjBeSgOQXp5Qe3SuBSSZD+CSggfpkSGEEOJKJLvVstVHGCt9OFZuptw8USLjWLx2uka15SfHMYZiOO8yUnTohBH7KxnmGz6mrqGWm3gMF1IMF1IopZipdRgvpcm7FsW0xUQ5CT6W2rWLDh6kR4YQQogrkQQfy+qdIDk+SVsEUcz+SoZKziXrmFSyNoM5l7GCg9JgbqlD04/R0fjp6TqGrlPvhMw3fZSmCN9QvO+qCsOFFJAENi9N1gij5GrsQCZpl17J2pcUPEiPDCGEEFciCT6WJXNZ2hydbRJEMaW0zd5KtlsEOpR3OTbfxNQNBgspGnNNxkoZHENjpJii6QW8PlvHasNsw2PPQKobfKzUZqRskx9NLtH0Q5baITfuzsvtFCGEEDuO3u8FXC4cU2fPQJp9gxkipZhaavOjk0vdkfer2bpOGClOV9ukHZPdAynqnZDZus9M3Wem5lNtBd2vX6nNeHuuAcDecoYoVhxfaPGjk0u8Pt0452sJIYQQ241kPpblXItS1mZyMWldvrecYbrm8ZOpJeYaHrah0QkisrbOqaUOrqVjmzr1TsArp2rUOiEayRXdfEqjmLa633ulNqOQMskutGgHEaausdj0OVXrsLecoR1E1DtJwLJd5rYIIYQQ65HgY9mZAcLpWofXTjd4a66BHypGii5L7QADjWrLJ2WZVHIOLT/CMHQO7S4w2/DIOxYTlUy3oBRW3ajJOYyX0hxfaLHY9Dmx0GKu6TNd8zgwmMELY96Svh1CCCG2OQk+zlDK2GQck9dO14iUIo41JpfalDIWYaQYzNsstByyrsGx+RaupZNxTdpBxE1jRcZLayfUrqZpGpqmsdQOOVXrMN/0uW5XnmrLZ7yUxjF16dshhBBi25PgY9mZ3ULTjpl0OdU0dA1m6h5KwXxTp5Ay0TUdRchg3iXnWFSyTjfoeKejku6MluVjnWrLZ/dAupspkb4dQgghtjvZ3Zad2S10IG1xYDBDreVTzli0Oj6FtEspY1DOZjhVbRNEFtcMZemEMeWss6Ejku6MliDiwGDmrEyJ9O0QQgix3UnwsWwlKJhcbCW9ONImB0fynFho8tJUjXonRjdDFtom7aDN6ZrHTD0ZMnfTWHHDWYr1GoOtzpRI3w4hhBDbXc+v2j7yyCPceuut5HI5hoaG+OhHP8qrr77a65fpuUrWZrTo4oURdS9gvukzVe3QCWLSlkHGMXh9usbR2TphFDFacLl6MEvesRgvpTecpVgpPt0/mL2gqbZCCCHEdtHz4OOpp57iyJEjPPvsszz55JMEQcCHP/xhms1mr1+qp+YaPpOLbU4utHn1VJ3Zhs/kYhMviAmimDdmGzQ6IdVmQM2LWGoHhEp1b7ZIECGEEEJsTM+PXf793/99za+/8pWvMDQ0xPPPP88HPvCBXr9czzS8kMVmQBDHnFpq89Z8i5GCzY2jBfYMuJystnHzDn4YE0cR79pbYiBtX1DWQwghhBBbUPOxtLQEQKlUWve/e56H5/2ss2etVtvsJa2hlqfZzjc85psdWn7EYM7h+EKLrGMx3woopWxsQ2euHpBPmWQdh6uGcuwfzG7pWoUQQojtYFPbq8dxzIMPPsidd97JoUOH1v2aRx55hEKh0P3Ys2fPZi7pLCtXbOcaHkEUo1CkbINy1mEg7QAa5azF4T1Frh/JMZxzKWetS74GuzLp9uhsg5lapzsJVwghhNjuNjXzceTIEV5++WWeeeaZc37Npz71KR566KHur2u12pYGIN2hb5bBXNPDREMHhnMOjqmxq5Di6uEckQLT0DA0jX2D2W4r9Atpgb6SZWl4IZ0gYnKxTayQbqZCCCF2lE0LPu6//36++c1v8vTTTzM2NnbOr3McB8fpX81ExjZoeAHfO7rEyYU2E6U0xxfaDOcddE3j4EiecsYGGuQciyhWTNc6tPz4goOG1Y3M5hoelq5zcDQv3UyFEELsKD0/dlFKcf/99/PEE0/w3e9+l3379vX6JXpOKVAoFBrVdkC1HWIaBg0/ouVHtIKYnGvxnokShq7R9CNGiymiWNHwwg2/zupGZqau4UeRdDPdYnLcJYQQ/dfzHe/IkSM8/vjjfOMb3yCXy3H69GkACoUCqVSq1y93yZp+RM61+MA1Q0SvzlJvexTTFgMpi2j5a1YakE1V22QcE03jooKG1d+nnLUZKbi0/ORVlFIopeTK7iY7s42+HHcJIcTW63nw8eijjwLwC7/wC2s+/9hjj/GJT3yi1y93yVYCgk4Yc9NYgaxjMLnYpumHmIZO2jaoZO1uV9KMbQBJ0LK6Bfrqeo71OpfC2d1NlVK8NFkjihVL7Ro3LTcgE5vnzDb6ctwlhBBbr+fBx5WWxj4zIKhkbX56us4LxxaxDYOpaofBnHvetucb+Yl6pbvpyvc5OtuQjXCLrc4+yXGXEEL0x45/5z0zIABwLYPBnHtBQcHF/EQtG+HWW2+2jhBCiK0lu906LiYouJjfIxvh1lsv2BRCCLG1JPhYx8UEBRfze2QjFEIIsRNJ8LGOiwkKJJAQQgghNmZT26sLIYQQQpxJgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWuuwGyymlAKjVan1eiRBCCCE2amXfXtnH38llF3zU63UA9uzZ0+eVCCGEEOJC1et1CoXCO36NpjYSomyhOI6Zmpoil8uhaVpPv3etVmPPnj2cOHGCfD7f0+99OZDnu7LJ8135tvszyvNd2Tb7+ZRS1Ot1RkdH0fV3ruq47DIfuq4zNja2qa+Rz+e35T+sFfJ8VzZ5vivfdn9Geb4r22Y+3/kyHiuk4FQIIYQQW0qCDyGEEEJsqR0VfDiOw8MPP4zjOP1eyqaQ57uyyfNd+bb7M8rzXdkup+e77ApOhRBCCLG97ajMhxBCCCH6T4IPIYQQQmwpCT6EEEIIsaUk+BBCCCHEltoxwccXv/hF9u7di+u63H777Xzve9/r95J65umnn+YjH/kIo6OjaJrG17/+9X4vqaceeeQRbr31VnK5HENDQ3z0ox/l1Vdf7feyeubRRx/lpptu6jb+ueOOO/jWt77V72Vtms9+9rNomsaDDz7Y76X0xB//8R+jadqaj+uuu67fy+qpyclJfuM3foNyuUwqleLGG2/k+9//fr+X1TN79+496+9Q0zSOHDnS76VdsiiK+KM/+iP27dtHKpXiwIED/Omf/umG5q9sph0RfPzjP/4jDz30EA8//DAvvPAChw8f5pd+6ZeYmZnp99J6otlscvjwYb74xS/2eymb4qmnnuLIkSM8++yzPPnkkwRBwIc//GGazWa/l9YTY2NjfPazn+X555/n+9//Pr/4i7/IL//yL/PjH/+430vrueeee44vfelL3HTTTf1eSk/dcMMNnDp1qvvxzDPP9HtJPbO4uMidd96JZVl861vf4ic/+Ql//ud/zsDAQL+X1jPPPffcmr+/J598EoCPfexjfV7Zpfvc5z7Ho48+yhe+8AVeeeUVPve5z/Fnf/ZnfP7zn+/vwtQOcNttt6kjR450fx1FkRodHVWPPPJIH1e1OQD1xBNP9HsZm2pmZkYB6qmnnur3UjbNwMCA+tu//dt+L6On6vW6uvrqq9WTTz6pfv7nf1498MAD/V5STzz88MPq8OHD/V7Gpvn93/999b73va/fy9hSDzzwgDpw4ICK47jfS7lkd999t7rvvvvWfO5XfuVX1D333NOnFSW2febD932ef/55PvShD3U/p+s6H/rQh/if//mfPq5MXKylpSUASqVSn1fSe1EU8dWvfpVms8kdd9zR7+X01JEjR7j77rvX/L+4Xbz++uuMjo6yf/9+7rnnHo4fP97vJfXMv/7rv3LLLbfwsY99jKGhId797nfzN3/zN/1e1qbxfZ+///u/57777uv5cNN+eO9738t3vvMdXnvtNQB++MMf8swzz3DXXXf1dV2X3WC5XpubmyOKIoaHh9d8fnh4mJ/+9Kd9WpW4WHEc8+CDD3LnnXdy6NChfi+nZ1566SXuuOMOOp0O2WyWJ554guuvv77fy+qZr371q7zwwgs899xz/V5Kz91+++185Stf4dprr+XUqVP8yZ/8Ce9///t5+eWXyeVy/V7eJTt69CiPPvooDz30EH/wB3/Ac889x2//9m9j2zb33ntvv5fXc1//+tepVqt84hOf6PdSeuKTn/wktVqN6667DsMwiKKIT3/609xzzz19Xde2Dz7E9nLkyBFefvnlbXWmDnDttdfy4osvsrS0xD/90z9x77338tRTT22LAOTEiRM88MADPPnkk7iu2+/l9NzqnyBvuukmbr/9diYmJvja177Gb/3Wb/VxZb0RxzG33HILn/nMZwB497vfzcsvv8xf//Vfb8vg4+/+7u+46667GB0d7fdSeuJrX/sa//AP/8Djjz/ODTfcwIsvvsiDDz7I6OhoX//+tn3wUalUMAyD6enpNZ+fnp5m165dfVqVuBj3338/3/zmN3n66acZGxvr93J6yrZtrrrqKgBuvvlmnnvuOf7qr/6KL33pS31e2aV7/vnnmZmZ4T3veU/3c1EU8fTTT/OFL3wBz/MwDKOPK+ytYrHINddcwxtvvNHvpfTEyMjIWUHwwYMH+ed//uc+rWjzHDt2jP/8z//kX/7lX/q9lJ75vd/7PT75yU/ya7/2awDceOONHDt2jEceeaSvwce2r/mwbZubb76Z73znO93PxXHMd77znW13pr5dKaW4//77eeKJJ/jud7/Lvn37+r2kTRfHMZ7n9XsZPfHBD36Ql156iRdffLH7ccstt3DPPffw4osvbqvAA6DRaPDmm28yMjLS76X0xJ133nnW1fbXXnuNiYmJPq1o8zz22GMMDQ1x991393spPdNqtdD1tVu9YRjEcdynFSW2feYD4KGHHuLee+/llltu4bbbbuMv//IvaTab/OZv/ma/l9YTjUZjzU9Zb731Fi+++CKlUonx8fE+rqw3jhw5wuOPP843vvENcrkcp0+fBqBQKJBKpfq8ukv3qU99irvuuovx8XHq9TqPP/44//3f/823v/3tfi+tJ3K53Fn1OZlMhnK5vC3qdn73d3+Xj3zkI0xMTDA1NcXDDz+MYRj8+q//er+X1hO/8zu/w3vf+14+85nP8Ku/+qt873vf48tf/jJf/vKX+720norjmMcee4x7770X09w+W+NHPvIRPv3pTzM+Ps4NN9zAD37wA/7iL/6C++67r78L6+tdmy30+c9/Xo2PjyvbttVtt92mnn322X4vqWf+67/+SwFnfdx77739XlpPrPdsgHrsscf6vbSeuO+++9TExISybVsNDg6qD37wg+o//uM/+r2sTbWdrtp+/OMfVyMjI8q2bbV792718Y9/XL3xxhv9XlZP/du//Zs6dOiQchxHXXfdderLX/5yv5fUc9/+9rcVoF599dV+L6WnarWaeuCBB9T4+LhyXVft379f/eEf/qHyPK+v69KU6nObMyGEEELsKNu+5kMIIYQQlxcJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFtKgg8hhBBCbCkJPoQQQgixpST4EEIIIcSWkuBDCCGEEFvq/wfPqAyP2kEskwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# similarly, for fever\n", + "sns.regplot(x=\"new_cases_percent_of_pop\", y=\"search_trends_fever\", data=weekly_data, scatter_kws={'alpha': 0.2, \"s\" :5})" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "id": "-S1A9E3WGaYH" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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HN57773tDqllHnzwwfD6669vdxjr5k/+5E/Cj33sY+0O4wNz7733hjt37gyDIGh3KC1x1113hQcOHFjysU9+8pPhPffc06aIwnBTVkYcx+HVV1/l9ttvb35MVVVuv/12fvKTn7QxMnEhCoUCAJ2dnW2OpPV83+d73/selUqFffv2tTucljp48CB33XXXkv8PN5Jf/OIXDAwMsGPHDu655x5GRkbaHVLL/Pu//zs333wzn/rUp+jp6eGGG27g7/7u79od1rpwHId//Md/5MCBAy29qLWdPvrRj/Lss8/y7rvvAvCzn/2MF154gTvvvLNtMV0WF+W12szMDL7v09vbu+Tjvb29vPPOO22KSlyIIAi47777uO2227juuuvaHU7LvPnmm+zbtw/Lskgmkzz11FNcc8017Q6rZb73ve/x2muv8fLLL7c7lHVx66238vjjj3P11VczPj7OF7/4RT7+8Y/z1ltvkUql2h3eRTt27BiPPPII999/P3/6p3/Kyy+/zB/8wR9gGAb79+9vd3gt9fTTT5PP5/md3/mddofSMl/4whcoFovs2rULTdPwfZ8vfelL3HPPPW2LaVMmI2LjOHjwIG+99daG2o8HuPrqqzl06BCFQoF//ud/Zv/+/Tz33HMbIiE5deoU9957L8888wzRaLTd4ayLxe8w9+7dy6233srw8DBPPvkkv/d7v9fGyFojCAJuvvlmvvzlLwNwww038NZbb/Gtb31rwyUjf//3f8+dd97JwMBAu0NpmSeffJLvfve7PPHEE1x77bUcOnSI++67j4GBgba9fpsyGenq6kLTNCYnJ5d8fHJykr6+vjZFJc7X5z73Of7zP/+T559/nsHBwXaH01KGYXDFFVcAcNNNN/Hyyy/z9a9/nW9/+9ttjuzivfrqq0xNTXHjjTc2P+b7Ps8//zwPP/wwtm2jaVobI2y9bDbLVVddxdGjR9sdSkv09/efkRjv3r2bf/mXf2lTROvj5MmT/M///A//+q//2u5QWuqP/uiP+MIXvsBv/uZvArBnzx5OnjzJQw891LZkZFP2jBiGwU033cSzzz7b/FgQBDz77LMbbl9+IwrDkM997nM89dRT/OhHP2L79u3tDmndBUGAbdvtDqMlPvGJT/Dmm29y6NCh5p+bb76Ze+65h0OHDm24RASgXC7z3nvv0d/f3+5QWuK222474zj9u+++y/DwcJsiWh+PPfYYPT093HXXXe0OpaWq1SqqunT51zSNIAjaFNEmrYwA3H///ezfv5+bb76ZW265hb/927+lUqnwu7/7u+0OrSXK5fKSd2HHjx/n0KFDdHZ2MjQ01MbILt7Bgwd54okn+Ld/+zdSqRQTExMAZDIZYrFYm6O7eA888AB33nknQ0NDlEolnnjiCX784x/zwx/+sN2htUQqlTqjvyeRSJDL5TZM388f/uEfcvfddzM8PMzY2BgPPvggmqbxW7/1W+0OrSU+//nP89GPfpQvf/nLfPrTn+all17i0Ucf5dFHH213aC0TBAGPPfYY+/fvR9c31lJ5991386UvfYmhoSGuvfZaXn/9df76r/+aAwcOtC+otp3juQR84xvfCIeGhkLDMMJbbrklfPHFF9sdUsv87//+bwic8Wf//v3tDu2irfS8gPCxxx5rd2gtceDAgXB4eDg0DCPs7u4OP/GJT4T//d//3e6w1tVGO9r7mc98Juzv7w8Nwwi3bNkSfuYznwmPHj3a7rBa6j/+4z/C6667LjRNM9y1a1f46KOPtjuklvrhD38YAuGRI0faHUrLFYvF8N577w2HhobCaDQa7tixI/yzP/uz0LbttsWkhGEbR64JIYQQYtPblD0jQgghhLh0SDIihBBCiLaSZEQIIYQQbSXJiBBCCCHaSpIRIYQQQrSVJCNCCCGEaCtJRoQQQgjRVpKMCCGEEKKtJBkRQgghRFtJMiKEEEKItpJkRAghhBBtJcmIEEIIIdrq/wHRnL6tjOlowAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# similarly, for bruise\n", + "sns.regplot(\n", + " x=\"new_cases_percent_of_pop\",\n", + " y=\"search_trends_bruise\",\n", + " data=weekly_data,\n", + " scatter_kws={'alpha': 0.2, \"s\" :5}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hd2A8707Uhz2" + }, + "source": [ + "We see that the slope of the line is positive in the graphs for cough and fever, but flat for bruise. That means that in places with increasing new cases of COVID-19, we saw increasing searches for cough and fever, but we didn't see increasing searches for unrelated symptoms like bruises. Interesting!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recap" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We used matplotlib to draw a line graph of COVID-19 cases over time in the USA. Then, we used downsampling to download only a portion of the available data, used seaborn to plot lines of best fit to observe corellation between COVID-19 cases and searches for related versus unrelated symptoms.\n", + "\n", + "Thank you for using BigQuery DataFrames!" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "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.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/visualization/tutorial.ipynb b/notebooks/visualization/tutorial.ipynb index ab838a89f0b..89a5ed87b8f 100644 --- a/notebooks/visualization/tutorial.ipynb +++ b/notebooks/visualization/tutorial.ipynb @@ -27,7 +27,7 @@ "id": "e661697d", "metadata": {}, "source": [ - "## BigQuery DataFrame Visualization Tutorials\n", + "# BigQuery DataFrame Visualization Tutorials", "\n", "\n", "\n", diff --git a/noxfile.py b/noxfile.py index 76400671af3..fc884903217 100644 --- a/noxfile.py +++ b/noxfile.py @@ -505,9 +505,10 @@ def docs(session): """Build the docs for this library.""" session.install("-e", ".[scikit-learn]") session.install( - "sphinx==8.2.3", + "sphinx==9.1.0", "sphinx-sitemap==2.9.0", - "myst-parser==4.0.1", + "myst-parser==5.0.0", + "myst-nb==1.4.0", "pydata-sphinx-theme==0.16.1", ) @@ -736,11 +737,7 @@ def notebook(session: nox.Session): notebooks_reg = { "regionalized.ipynb": [ "asia-southeast1", - "eu", - "europe-west4", - "southamerica-west1", "us", - "us-central1", ] } notebooks_reg = { diff --git a/tests/js/package-lock.json b/tests/js/package-lock.json index 5526e0581e2..04dea0fb8c1 100644 --- a/tests/js/package-lock.json +++ b/tests/js/package-lock.json @@ -12,7 +12,7 @@ "@babel/preset-env": "^7.24.7", "@testing-library/jest-dom": "^6.4.6", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "jsdom": "^24.1.0" } }, @@ -2105,6 +2105,215 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", @@ -2166,6 +2375,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2481,16 +2714,6 @@ "yarn": ">=1" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2574,9 +2797,9 @@ } }, "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, "license": "MIT", "dependencies": { @@ -2626,51 +2849,6 @@ "dev": true, "license": "MIT" }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3351,13 +3529,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true, - "license": "MIT" - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -3480,20 +3651,6 @@ "dev": true, "license": "MIT" }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3611,28 +3768,6 @@ "node": ">=6" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -3647,16 +3782,6 @@ "node": ">=4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4499,26 +4624,23 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" + "jsdom": "^26.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -4526,135 +4648,192 @@ } } }, - "node_modules/jest-environment-jsdom/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "license": "MIT", "dependencies": { - "debug": "4" + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" }, "engines": { - "node": ">= 6.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "license": "MIT", "dependencies": { - "cssom": "~0.3.6" + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/jest-environment-jsdom/node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^2.0.0" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" }, "engines": { - "node": ">= 6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", - "debug": "4" + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -4662,77 +4841,52 @@ } } }, - "node_modules/jest-environment-jsdom/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, "engines": { "node": ">=12" - } - }, - "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" }, - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.6.3" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "node_modules/jest-environment-jsdom/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" + "tldts": "^6.1.32" }, "engines": { - "node": ">=12" - } - }, - "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12" + "node": ">=16" } }, "node_modules/jest-environment-node": { @@ -6178,6 +6332,26 @@ "node": "*" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/tests/js/package.json b/tests/js/package.json index d34c5a065aa..42399e96fd4 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@babel/preset-env": "^7.24.7", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.2.0", "@testing-library/jest-dom": "^6.4.6", "jsdom": "^24.1.0" } diff --git a/tests/system/large/bigquery/test_ml.py b/tests/system/large/bigquery/test_ml.py index 20a62ae2b64..f0f7d4f6917 100644 --- a/tests/system/large/bigquery/test_ml.py +++ b/tests/system/large/bigquery/test_ml.py @@ -64,6 +64,32 @@ def test_generate_embedding_with_options(embedding_model): assert len(embedding[0]) == 256 +def test_get_insights(dataset_id): + df = bpd.DataFrame( + { + "dim1": ["a", "a", "b", "b", "a", "a", "b", "b"], + "dim2": ["x", "y", "x", "y", "x", "y", "x", "y"], + "metric": [10, 20, 30, 40, 12, 25, 35, 45], + "is_test": [False, False, False, False, True, True, True, True], + } + ) + model_name = f"{dataset_id}.contribution_analysis_model" + + ml.create_model( + model_name=model_name, + options={ + "model_type": "CONTRIBUTION_ANALYSIS", + "contribution_metric": "SUM(metric)", + "is_test_col": "is_test", + }, + training_data=df, + ) + + result = ml.get_insights(model_name) + assert len(result) > 0 + assert "contributors" in result.columns + + def test_create_model_linear_regression(dataset_id): df = bpd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 6]}) model_name = f"{dataset_id}.linear_regression_model" diff --git a/tests/system/large/functions/test_remote_function.py b/tests/system/large/functions/test_remote_function.py index 079b909e7aa..114b600d9de 100644 --- a/tests/system/large/functions/test_remote_function.py +++ b/tests/system/large/functions/test_remote_function.py @@ -2629,12 +2629,6 @@ def generate_stats(row: pandas.Series) -> list[int]: True, id="set-none", ), - pytest.param( - {"cloud_function_ingress_settings": "all"}, - functions_v2.ServiceConfig.IngressSettings.ALLOW_ALL, - False, - id="set-all", - ), pytest.param( {"cloud_function_ingress_settings": "internal-only"}, functions_v2.ServiceConfig.IngressSettings.ALLOW_INTERNAL_ONLY, @@ -2699,6 +2693,25 @@ def square(x: int) -> int: ) +@pytest.mark.flaky(retries=2, delay=120) +def test_remote_function_ingress_settings_w_all(session): + ingress_settings_args = {"cloud_function_ingress_settings": "all"} + + with pytest.raises( + google.api_core.exceptions.FailedPrecondition, + match="400.*allowedIngress violated", + ): + + def square(x: int) -> int: + return x * x + + session.remote_function( + reuse=False, + cloud_function_service_account="default", + **ingress_settings_args, + )(square) + + @pytest.mark.flaky(retries=2, delay=120) def test_remote_function_ingress_settings_unsupported(session): with pytest.raises( diff --git a/tests/system/small/bigquery/test_datetime.py b/tests/system/small/bigquery/test_datetime.py index 789ae47ae2e..ff9bcb38a07 100644 --- a/tests/system/small/bigquery/test_datetime.py +++ b/tests/system/small/bigquery/test_datetime.py @@ -19,7 +19,7 @@ import pytest from bigframes import bigquery -import bigframes.testing +import bigframes.testing.utils _TIMESTAMP_DTYPE = pd.ArrowDtype(pa.timestamp("us", tz="UTC")) @@ -41,7 +41,7 @@ def test_unix_seconds(scalars_dfs): .apply(lambda ts: _to_unix_epoch(ts, "s")) .astype("Int64") ) - bigframes.testing.assert_series_equal(actual_res, expected_res) + bigframes.testing.utils.assert_series_equal(actual_res, expected_res) def test_unix_seconds_after_type_casting(int_series): @@ -54,7 +54,7 @@ def test_unix_seconds_after_type_casting(int_series): .apply(lambda ts: _to_unix_epoch(ts, "s")) .astype("Int64") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_res, expected_res, check_index_type=False ) @@ -76,7 +76,7 @@ def test_unix_millis(scalars_dfs): .apply(lambda ts: _to_unix_epoch(ts, "ms")) .astype("Int64") ) - bigframes.testing.assert_series_equal(actual_res, expected_res) + bigframes.testing.utils.assert_series_equal(actual_res, expected_res) def test_unix_millis_after_type_casting(int_series): @@ -89,7 +89,7 @@ def test_unix_millis_after_type_casting(int_series): .apply(lambda ts: _to_unix_epoch(ts, "ms")) .astype("Int64") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_res, expected_res, check_index_type=False ) @@ -111,7 +111,7 @@ def test_unix_micros(scalars_dfs): .apply(lambda ts: _to_unix_epoch(ts, "us")) .astype("Int64") ) - bigframes.testing.assert_series_equal(actual_res, expected_res) + bigframes.testing.utils.assert_series_equal(actual_res, expected_res) def test_unix_micros_after_type_casting(int_series): @@ -124,7 +124,7 @@ def test_unix_micros_after_type_casting(int_series): .apply(lambda ts: _to_unix_epoch(ts, "us")) .astype("Int64") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_res, expected_res, check_index_type=False ) diff --git a/tests/system/small/bigquery/test_geo.py b/tests/system/small/bigquery/test_geo.py index 24ecb7f6394..021947eb9ed 100644 --- a/tests/system/small/bigquery/test_geo.py +++ b/tests/system/small/bigquery/test_geo.py @@ -32,7 +32,7 @@ import bigframes.bigquery as bbq import bigframes.geopandas import bigframes.session -import bigframes.testing +import bigframes.testing.utils def test_geo_st_area(session: bigframes.session.Session): @@ -57,7 +57,7 @@ def test_geo_st_area(session: bigframes.session.Session): geobf_s_result = bbq.st_area(geobf_s).to_pandas().round(-3) assert geobf_s_result.iloc[0] >= 1000 - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, geopd_s_result, check_dtype=False, @@ -110,7 +110,7 @@ def test_st_length_various_geometries(session): # Test default use_spheroid result_default = st_length(geoseries).to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( result_default, expected_lengths, rtol=1e-3, @@ -119,7 +119,7 @@ def test_st_length_various_geometries(session): # Test explicit use_spheroid=False result_explicit_false = st_length(geoseries, use_spheroid=False).to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( result_explicit_false, expected_lengths, rtol=1e-3, @@ -153,7 +153,7 @@ def test_geo_st_difference_with_geometry_objects(session: bigframes.session.Sess index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -192,7 +192,7 @@ def test_geo_st_difference_with_single_geometry_object( index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -218,7 +218,7 @@ def test_geo_st_difference_with_similar_geometry_objects( index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -274,7 +274,7 @@ def test_geo_st_distance_with_geometry_objects(session: bigframes.session.Sessio index=[0, 1, 2, 3], dtype="Float64", ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -321,7 +321,7 @@ def test_geo_st_distance_with_single_geometry_object( ], dtype="Float64", ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -356,7 +356,7 @@ def test_geo_st_intersection_with_geometry_objects(session: bigframes.session.Se index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -395,7 +395,7 @@ def test_geo_st_intersection_with_single_geometry_object( index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -425,7 +425,7 @@ def test_geo_st_intersection_with_similar_geometry_objects( index=[0, 1, 2], dtype=geopandas.array.GeometryDtype(), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( geobf_s_result, expected, check_index_type=False, @@ -466,7 +466,7 @@ def test_geo_st_isclosed(session: bigframes.session.Session): ] expected_series = pd.Series(data=expected_data, dtype="boolean") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, expected_series, # We default to Int64 (nullable) dtype, but pandas defaults to int64 index. diff --git a/tests/system/small/bigquery/test_mathematical.py b/tests/system/small/bigquery/test_mathematical.py new file mode 100644 index 00000000000..66aef96e57d --- /dev/null +++ b/tests/system/small/bigquery/test_mathematical.py @@ -0,0 +1,37 @@ +# 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 bigframes.bigquery as bbq + + +def test_rand(scalars_df_index): + df = scalars_df_index + + # Apply rand + df = df.assign(random=bbq.rand()) + result = df["random"] + + # Eagerly evaluate + result_pd = result.to_pandas() + + # Check length + assert len(result_pd) == len(df) + + # Check values in [0, 1) + assert (result_pd >= 0).all() + assert (result_pd < 1).all() + + # Check not all values are equal (unlikely collision for random) + if len(result_pd) > 1: + assert result_pd.nunique() > 1 diff --git a/tests/system/small/bigquery/test_sql.py b/tests/system/small/bigquery/test_sql.py index fa43c249658..c0f7eed938e 100644 --- a/tests/system/small/bigquery/test_sql.py +++ b/tests/system/small/bigquery/test_sql.py @@ -17,7 +17,7 @@ import bigframes.bigquery as bbq import bigframes.dtypes as dtypes import bigframes.pandas as bpd -import bigframes.testing +import bigframes.testing.utils def test_sql_scalar_for_all_scalar_types(scalars_df_null_index): @@ -60,7 +60,9 @@ def test_sql_scalar_for_bool_series(scalars_df_index): result = bbq.sql_scalar("CAST({0} AS INT64)", [series]) expected = series.astype(dtypes.INT_DTYPE) expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) @pytest.mark.parametrize( @@ -84,7 +86,9 @@ def test_sql_scalar_outputs_all_scalar_types(scalars_df_index, column_name): result = bbq.sql_scalar("{0}", [series]) expected = series expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_for_array_series(repeated_df): @@ -114,14 +118,18 @@ def test_sql_scalar_for_array_series(repeated_df): + repeated_df["numeric_list_col"].list.len() + repeated_df["string_list_col"].list.len() ) - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_outputs_array_series(repeated_df): result = bbq.sql_scalar("{0}", [repeated_df["int_list_col"]]) expected = repeated_df["int_list_col"] expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_for_struct_series(nested_structs_df): @@ -132,14 +140,18 @@ def test_sql_scalar_for_struct_series(nested_structs_df): expected = nested_structs_df["person"].struct.field( "name" ).str.len() + nested_structs_df["person"].struct.field("age") - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_outputs_struct_series(nested_structs_df): result = bbq.sql_scalar("{0}", [nested_structs_df["person"]]) expected = nested_structs_df["person"] expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_for_json_series(json_df): @@ -151,11 +163,15 @@ def test_sql_scalar_for_json_series(json_df): ) expected = bbq.json_value(json_df["json_col"], "$.int_value") expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) def test_sql_scalar_outputs_json_series(json_df): result = bbq.sql_scalar("{0}", [json_df["json_col"]]) expected = json_df["json_col"] expected.name = result.name - bigframes.testing.assert_series_equal(result.to_pandas(), expected.to_pandas()) + bigframes.testing.utils.assert_series_equal( + result.to_pandas(), expected.to_pandas() + ) diff --git a/tests/system/small/bigquery/test_struct.py b/tests/system/small/bigquery/test_struct.py index 5e51a5fce04..85404969605 100644 --- a/tests/system/small/bigquery/test_struct.py +++ b/tests/system/small/bigquery/test_struct.py @@ -16,7 +16,7 @@ import bigframes.bigquery as bbq import bigframes.series as series -import bigframes.testing +import bigframes.testing.utils @pytest.mark.parametrize( @@ -53,7 +53,7 @@ def test_struct_from_dataframe(columns_arg): srs = series.Series( columns_arg, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( srs.to_pandas(), bbq.struct(srs.struct.explode()).to_pandas(), check_index_type=False, diff --git a/tests/system/small/core/test_reshape.py b/tests/system/small/core/test_reshape.py index 4d20ce887a7..519ed91fd32 100644 --- a/tests/system/small/core/test_reshape.py +++ b/tests/system/small/core/test_reshape.py @@ -17,7 +17,7 @@ from bigframes import session from bigframes.core.reshape import merge -import bigframes.testing +import bigframes.testing.utils @pytest.mark.parametrize( @@ -56,7 +56,7 @@ def test_join_with_index( how=how, ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) diff --git a/tests/system/small/ml/test_metrics.py b/tests/system/small/ml/test_metrics.py index 5c595898164..6675745ee63 100644 --- a/tests/system/small/ml/test_metrics.py +++ b/tests/system/small/ml/test_metrics.py @@ -20,7 +20,7 @@ import bigframes from bigframes.ml import metrics -import bigframes.testing +import bigframes.testing.utils def test_r2_score_perfect_fit(session): @@ -162,7 +162,7 @@ def test_roc_curve_binary_classification_prediction_returns_expected(session): pd_tpr = tpr.to_pandas() pd_thresholds = thresholds.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions pd_thresholds[1:], pd.Series( @@ -172,7 +172,7 @@ def test_roc_curve_binary_classification_prediction_returns_expected(session): ), check_index=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_fpr, pd.Series( [0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 0.75, 1.0], @@ -181,7 +181,7 @@ def test_roc_curve_binary_classification_prediction_returns_expected(session): ), check_index_type=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_tpr, pd.Series( [ @@ -262,7 +262,7 @@ def test_roc_curve_binary_classification_decision_returns_expected(session): pd_tpr = tpr.to_pandas() pd_thresholds = thresholds.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions pd_thresholds[1:], pd.Series( @@ -272,7 +272,7 @@ def test_roc_curve_binary_classification_decision_returns_expected(session): ), check_index=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_fpr, pd.Series( [0.0, 0.0, 1.0], @@ -281,7 +281,7 @@ def test_roc_curve_binary_classification_decision_returns_expected(session): ), check_index_type=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_tpr, pd.Series( [ @@ -354,7 +354,7 @@ def test_roc_curve_binary_classification_prediction_series(session): pd_tpr = tpr.to_pandas() pd_thresholds = thresholds.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( # skip testing the first value, as it is redundant and inconsistent across sklearn versions pd_thresholds[1:], pd.Series( @@ -364,7 +364,7 @@ def test_roc_curve_binary_classification_prediction_series(session): ), check_index=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_fpr, pd.Series( [0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 0.75, 1.0], @@ -373,7 +373,7 @@ def test_roc_curve_binary_classification_prediction_series(session): ), check_index_type=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_tpr, pd.Series( [ @@ -506,7 +506,7 @@ def test_confusion_matrix(session): 2: [0, 1, 2], } ).astype("int64") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -524,7 +524,7 @@ def test_confusion_matrix_column_index(session): {1: [1, 0, 1, 0], 2: [0, 0, 2, 0], 3: [0, 0, 0, 0], 4: [0, 1, 0, 1]}, index=[1, 2, 3, 4], ).astype("int64") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -543,7 +543,7 @@ def test_confusion_matrix_matches_sklearn(session): pd_df[["y_true"]], pd_df[["y_pred"]] ) expected_pd_df = pd.DataFrame(expected_confusion_matrix) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -565,7 +565,7 @@ def test_confusion_matrix_str_matches_sklearn(session): expected_confusion_matrix, index=["ant", "bird", "cat"] ) expected_pd_df.columns = pd.Index(["ant", "bird", "cat"]) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -586,7 +586,7 @@ def test_confusion_matrix_series(session): 2: [0, 1, 2], } ).astype("int64") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( confusion_matrix, expected_pd_df, check_index_type=False ) @@ -606,7 +606,7 @@ def test_recall_score(session): expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( recall, expected_recall, check_index_type=False ) @@ -626,7 +626,7 @@ def test_recall_score_matches_sklearn(session): ) expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( recall, expected_recall, check_index_type=False ) @@ -646,7 +646,7 @@ def test_recall_score_str_matches_sklearn(session): ) expected_index = ["ant", "bird", "cat"] expected_recall = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( recall, expected_recall, check_index_type=False ) @@ -664,7 +664,7 @@ def test_recall_score_series(session): expected_index = [0, 1, 2] expected_recall = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( recall, expected_recall, check_index_type=False ) @@ -684,7 +684,7 @@ def test_precision_score(session): expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( precision_score, expected_precision, check_index_type=False ) @@ -707,7 +707,7 @@ def test_precision_score_matches_sklearn(session): ) expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( precision_score, expected_precision, check_index_type=False ) @@ -729,7 +729,7 @@ def test_precision_score_str_matches_sklearn(session): ) expected_index = ["ant", "bird", "cat"] expected_precision = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( precision_score, expected_precision, check_index_type=False ) @@ -747,7 +747,7 @@ def test_precision_score_series(session): expected_index = [0, 1, 2] expected_precision = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( precision_score, expected_precision, check_index_type=False ) @@ -831,7 +831,9 @@ def test_f1_score(session): expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + f1_score, expected_f1, check_index_type=False + ) def test_f1_score_matches_sklearn(session): @@ -849,7 +851,9 @@ def test_f1_score_matches_sklearn(session): ) expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + f1_score, expected_f1, check_index_type=False + ) def test_f1_score_str_matches_sklearn(session): @@ -867,7 +871,9 @@ def test_f1_score_str_matches_sklearn(session): ) expected_index = ["ant", "bird", "cat"] expected_f1 = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + f1_score, expected_f1, check_index_type=False + ) def test_f1_score_series(session): @@ -883,7 +889,9 @@ def test_f1_score_series(session): expected_index = [0, 1, 2] expected_f1 = pd.Series(expected_values, index=expected_index) - bigframes.testing.assert_series_equal(f1_score, expected_f1, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + f1_score, expected_f1, check_index_type=False + ) def test_mean_squared_error(session: bigframes.Session): diff --git a/tests/system/small/ml/test_utils.py b/tests/system/small/ml/test_utils.py index 8d757549005..ec3bd315b13 100644 --- a/tests/system/small/ml/test_utils.py +++ b/tests/system/small/ml/test_utils.py @@ -16,7 +16,7 @@ import pytest import bigframes.ml.utils as utils -import bigframes.testing +import bigframes.testing.utils _DATA_FRAME = pd.DataFrame({"column": [1, 2, 3]}) _SERIES = pd.Series([1, 2, 3], name="column") @@ -31,7 +31,7 @@ def test_convert_to_dataframe(session, data): (actual_result,) = utils.batch_convert_to_dataframe(bf_data) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result.to_pandas(), _DATA_FRAME, check_index_type=False, @@ -46,7 +46,7 @@ def test_convert_to_dataframe(session, data): def test_convert_pandas_to_dataframe(data, session): (actual_result,) = utils.batch_convert_to_dataframe(data, session=session) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result.to_pandas(), _DATA_FRAME, check_index_type=False, @@ -63,7 +63,7 @@ def test_convert_to_series(session, data): (actual_result,) = utils.batch_convert_to_series(bf_data) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result.to_pandas(), _SERIES, check_index_type=False, check_dtype=False ) @@ -75,6 +75,6 @@ def test_convert_to_series(session, data): def test_convert_pandas_to_series(data, session): (actual_result,) = utils.batch_convert_to_series(data, session=session) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result.to_pandas(), _SERIES, check_index_type=False, check_dtype=False ) diff --git a/tests/system/small/operations/test_dates.py b/tests/system/small/operations/test_dates.py index e9f5f07d282..bce638d5372 100644 --- a/tests/system/small/operations/test_dates.py +++ b/tests/system/small/operations/test_dates.py @@ -20,7 +20,7 @@ import pytest from bigframes import dtypes -import bigframes.testing +import bigframes.testing.utils def test_date_diff_between_series(session): @@ -35,7 +35,7 @@ def test_date_diff_between_series(session): actual_result = (bf_df["col_1"] - bf_df["col_2"]).to_pandas() expected_result = (pd_df["col_1"] - pd_df["col_2"]).astype(dtypes.TIMEDELTA_DTYPE) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -47,7 +47,7 @@ def test_date_diff_literal_sub_series(scalars_dfs): actual_result = (literal - bf_df["date_col"]).to_pandas() expected_result = (literal - pd_df["date_col"]).astype(dtypes.TIMEDELTA_DTYPE) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -59,7 +59,7 @@ def test_date_diff_series_sub_literal(scalars_dfs): actual_result = (bf_df["date_col"] - literal).to_pandas() expected_result = (pd_df["date_col"] - literal).astype(dtypes.TIMEDELTA_DTYPE) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -70,7 +70,7 @@ def test_date_series_diff_agg(scalars_dfs): actual_result = bf_df["date_col"].diff().to_pandas() expected_result = pd_df["date_col"].diff().astype(dtypes.TIMEDELTA_DTYPE) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -86,6 +86,6 @@ def test_date_can_cast_after_accessor(scalars_dfs): pd.to_datetime(pd_df["date_col"]).dt.isocalendar().week.astype("Int64") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False, check_index_type=False ) diff --git a/tests/system/small/operations/test_datetimes.py b/tests/system/small/operations/test_datetimes.py index 9f4b5e57054..2bc972a048b 100644 --- a/tests/system/small/operations/test_datetimes.py +++ b/tests/system/small/operations/test_datetimes.py @@ -342,19 +342,20 @@ def test_dt_tz_localize(scalars_dfs, col_name, tz): assert_series_equal(bf_result.to_pandas(), pd_result, check_index_type=False) -@pytest.mark.parametrize( - ("col_name", "tz"), - [ - ("timestamp_col", "UTC"), - ("datetime_col", "US/Eastern"), - ], -) -def test_dt_tz_localize_invalid_inputs(scalars_dfs, col_name, tz): +def test_dt_tz_localize_already_localized(scalars_dfs): + pytest.importorskip("pandas", minversion="2.0.0") + scalars_df, _ = scalars_dfs + + with pytest.raises(TypeError): + scalars_df["timestamp_col"].dt.tz_localize("UTC") + + +def test_dt_tz_localize_invalid_timezone(scalars_dfs): pytest.importorskip("pandas", minversion="2.0.0") scalars_df, _ = scalars_dfs with pytest.raises(ValueError): - scalars_df[col_name].dt.tz_localize(tz) + scalars_df["datetime_col"].dt.tz_localize("US/Eastern") @pytest.mark.parametrize( diff --git a/tests/system/small/operations/test_timedeltas.py b/tests/system/small/operations/test_timedeltas.py index 0329aece058..429c813220b 100644 --- a/tests/system/small/operations/test_timedeltas.py +++ b/tests/system/small/operations/test_timedeltas.py @@ -23,7 +23,7 @@ import pytest from bigframes import dtypes -import bigframes.testing +import bigframes.testing.utils # Some methods/features used by this test don't exist in pandas 1.x pytest.importorskip("pandas", minversion="2.0.0") @@ -87,7 +87,7 @@ def temporal_dfs(session): def _assert_series_equal(actual: pd.Series, expected: pd.Series): """Helper function specifically for timedelta testing. Don't use it outside of this module.""" - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual, expected, check_index_type=False, @@ -212,7 +212,7 @@ def test_timestamp_add__ts_series_plus_td_series(temporal_dfs, column, pd_dtype) actual_result = (bf_df[column] + bf_df["timedelta_col_1"]).to_pandas() expected_result = pd_df[column] + pd_df["timedelta_col_1"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -241,7 +241,7 @@ def test_timestamp_add__ts_series_plus_td_literal(temporal_dfs, literal): actual_result = (bf_df["timestamp_col"] + literal).to_pandas() expected_result = pd_df["timestamp_col"] + literal - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -259,7 +259,7 @@ def test_timestamp_add__td_series_plus_ts_series(temporal_dfs, column, pd_dtype) actual_result = (bf_df["timedelta_col_1"] + bf_df[column]).to_pandas() expected_result = pd_df["timedelta_col_1"] + pd_df[column] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -271,7 +271,7 @@ def test_timestamp_add__td_literal_plus_ts_series(temporal_dfs): actual_result = (timedelta + bf_df["datetime_col"]).to_pandas() expected_result = timedelta + pd_df["datetime_col"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -283,7 +283,7 @@ def test_timestamp_add__ts_literal_plus_td_series(temporal_dfs): actual_result = (timestamp + bf_df["timedelta_col_1"]).to_pandas() expected_result = timestamp + pd_df["timedelta_col_1"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -301,7 +301,7 @@ def test_timestamp_add_with_numpy_op(temporal_dfs, column, pd_dtype): actual_result = np.add(bf_df[column], bf_df["timedelta_col_1"]).to_pandas() expected_result = np.add(pd_df[column], pd_df["timedelta_col_1"]) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -316,7 +316,7 @@ def test_timestamp_add_dataframes(temporal_dfs): actual_result["timestamp_col"] = actual_result["timestamp_col"] expected_result = pd_df[columns] + timedelta - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_result, check_index_type=False ) @@ -337,7 +337,7 @@ def test_timestamp_sub__ts_series_minus_td_series( actual_result = (bf_df[column] - bf_df["timedelta_col_1"]).to_pandas() expected_result = pd_df[column] - pd_df["timedelta_col_1"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -360,7 +360,7 @@ def test_timestamp_sub__ts_series_minus_td_literal( # pandas type behavior changes per pandas version expected_result = (pd_df[column] - literal).astype(actual_result.dtype) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -374,7 +374,7 @@ def test_timestamp_sub__ts_literal_minus_td_series(temporal_dfs): ).to_pandas() # .astype(" pd.Timedelta(1, "h")] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -584,7 +584,7 @@ def test_timedelta_ordering(session): actual_result = (bf_df["col_2"] - bf_df["col_1"]).sort_values().to_pandas() expected_result = (pd_df["col_2"] - pd_df["col_1"]).sort_values() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -651,6 +651,6 @@ def test_timestamp_diff_after_type_casting(temporal_dfs): expected_result = pd_df["timestamp_col"] - pd_df["positive_int_col"].astype( "datetime64[us, UTC]" ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False, check_dtype=False ) diff --git a/tests/system/small/pandas/test_describe.py b/tests/system/small/pandas/test_describe.py index 6f288115128..b8e427c10ea 100644 --- a/tests/system/small/pandas/test_describe.py +++ b/tests/system/small/pandas/test_describe.py @@ -15,6 +15,8 @@ import pandas.testing import pytest +import bigframes.pandas as bpd + def test_df_describe_non_temporal(scalars_dfs): # TODO: supply a reason why this isn't compatible with pandas 1.x @@ -352,3 +354,40 @@ def test_series_groupby_describe(scalars_dfs): check_dtype=False, check_index_type=False, ) + + +def test_describe_json_and_obj_ref_returns_count(session): + # Test describe() works on JSON and OBJ_REF types (without nunique, which fails) + sql = """ + SELECT + PARSE_JSON('{"a": 1}') AS json_col, + 'gs://cloud-samples-data/vision/ocr/sign.jpg' AS uri_col + """ + df = session.read_gbq(sql) + + df["obj_ref_col"] = df["uri_col"].str.to_blob() + df = df.drop(columns=["uri_col"]) + + res = df.describe(include="all").to_pandas() + + assert "count" in res.index + assert res.loc["count", "json_col"] == 1.0 + assert res.loc["count", "obj_ref_col"] == 1.0 + + +def test_describe_with_unsupported_type_returns_empty_dataframe(session): + df = session.read_gbq("SELECT ST_GEOGPOINT(1.0, 2.0) AS geo_col") + + res = df.describe().to_pandas() + + assert len(res.columns) == 0 + assert len(res.index) == 1 + + +def test_describe_empty_dataframe_returns_empty_dataframe(session): + df = bpd.DataFrame() + + res = df.describe().to_pandas() + + assert len(res.columns) == 0 + assert len(res.index) == 1 diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index 8caeabb98bc..9683a8bc52d 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -134,7 +134,7 @@ def test_df_construct_structs(session): ] ).to_frame() bf_series = session.read_pandas(pd_frame) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_series.to_pandas(), pd_frame, check_index_type=False, check_dtype=False ) @@ -144,7 +144,7 @@ def test_df_construct_local_concat_pd(scalars_pandas_df_index, session): bf_df = session.read_pandas(pd_df) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_df.to_pandas(), pd_df, check_index_type=False, check_dtype=False ) @@ -319,7 +319,7 @@ def test_df_nlargest(scalars_df_index, scalars_pandas_df_index, keep): 3, ["bool_col", "int64_too"], keep=keep ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -337,7 +337,7 @@ def test_df_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): bf_result = scalars_df_index.nsmallest(6, ["bool_col"], keep=keep) pd_result = scalars_pandas_df_index.nsmallest(6, ["bool_col"], keep=keep) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -356,7 +356,7 @@ def test_get_columns(scalars_dfs): col_names = ["bool_col", "float64_col", "int64_col"] df_subset = scalars_df.get(col_names) df_pandas = df_subset.to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df[col_names].columns ) @@ -403,7 +403,9 @@ def test_insert(scalars_dfs, loc, column, value, allow_duplicates): bf_df.insert(loc, column, value, allow_duplicates) pd_df.insert(loc, column, value, allow_duplicates) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df, check_dtype=False) + bigframes.testing.utils.assert_frame_equal( + bf_df.to_pandas(), pd_df, check_dtype=False + ) def test_mask_series_cond(scalars_df_index, scalars_pandas_df_index): @@ -597,7 +599,7 @@ def test_drop_column(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name = "int64_col" df_pandas = scalars_df.drop(columns=col_name).to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df.drop(columns=col_name).columns ) @@ -606,7 +608,7 @@ def test_drop_columns(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_names = ["int64_col", "geography_col", "time_col"] df_pandas = scalars_df.drop(columns=col_names).to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df.drop(columns=col_names).columns ) @@ -618,7 +620,7 @@ def test_drop_labels_axis_1(scalars_dfs): pd_result = scalars_pandas_df.drop(labels=labels, axis=1) bf_result = scalars_df.drop(labels=labels, axis=1).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_with_custom_column_labels(scalars_dfs): @@ -645,7 +647,7 @@ def test_df_memory_usage(scalars_dfs): pd_result = scalars_pandas_df.memory_usage() bf_result = scalars_df.memory_usage() - bigframes.testing.assert_series_equal(pd_result, bf_result, rtol=1.5) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, rtol=1.5) def test_df_info(scalars_dfs): @@ -744,7 +746,7 @@ def test_select_dtypes(scalars_dfs, include, exclude): pd_result = scalars_pandas_df.select_dtypes(include=include, exclude=exclude) bf_result = scalars_df.select_dtypes(include=include, exclude=exclude).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_index(scalars_dfs): @@ -753,7 +755,7 @@ def test_drop_index(scalars_dfs): pd_result = scalars_pandas_df.drop(index=[4, 1, 2]) bf_result = scalars_df.drop(index=[4, 1, 2]).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_pandas_index(scalars_dfs): @@ -763,7 +765,7 @@ def test_drop_pandas_index(scalars_dfs): pd_result = scalars_pandas_df.drop(index=drop_index) bf_result = scalars_df.drop(index=drop_index).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_bigframes_index(scalars_dfs): @@ -774,7 +776,7 @@ def test_drop_bigframes_index(scalars_dfs): pd_result = scalars_pandas_df.drop(index=drop_pandas_index) bf_result = scalars_df.drop(index=drop_index).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_bigframes_index_with_na(scalars_dfs): @@ -791,7 +793,7 @@ def test_drop_bigframes_index_with_na(scalars_dfs): pd_result = scalars_pandas_df.drop(index=drop_pandas_index) # drop_pandas_index) bf_result = scalars_df.drop(index=drop_index).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_bigframes_multiindex(scalars_dfs): @@ -812,7 +814,7 @@ def test_drop_bigframes_multiindex(scalars_dfs): bf_result = scalars_df.drop(index=drop_index).to_pandas() pd_result = scalars_pandas_df.drop(index=drop_pandas_index) - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_labels_axis_0(scalars_dfs): @@ -821,7 +823,7 @@ def test_drop_labels_axis_0(scalars_dfs): pd_result = scalars_pandas_df.drop(labels=[4, 1, 2], axis=0) bf_result = scalars_df.drop(labels=[4, 1, 2], axis=0).to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_drop_index_and_columns(scalars_dfs): @@ -830,14 +832,14 @@ def test_drop_index_and_columns(scalars_dfs): pd_result = scalars_pandas_df.drop(index=[4, 1, 2], columns="int64_col") bf_result = scalars_df.drop(index=[4, 1, 2], columns="int64_col").to_pandas() - bigframes.testing.assert_frame_equal(pd_result, bf_result) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result) def test_rename(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"bool_col": 1.2345} df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -847,7 +849,9 @@ def test_df_peek(scalars_dfs_maybe_ordered): peek_result = scalars_df.peek(n=3, force=False, allow_large_results=True) - bigframes.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) + bigframes.testing.utils.assert_index_equal( + scalars_pandas_df.columns, peek_result.columns + ) assert len(peek_result) == 3 @@ -856,14 +860,18 @@ def test_df_peek_with_large_results_not_allowed(scalars_dfs_maybe_ordered): peek_result = scalars_df.peek(n=3, force=False, allow_large_results=False) - bigframes.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) + bigframes.testing.utils.assert_index_equal( + scalars_pandas_df.columns, peek_result.columns + ) assert len(peek_result) == 3 def test_df_peek_filtered(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs peek_result = scalars_df[scalars_df.int64_col != 0].peek(n=3, force=False) - bigframes.testing.assert_index_equal(scalars_pandas_df.columns, peek_result.columns) + bigframes.testing.utils.assert_index_equal( + scalars_pandas_df.columns, peek_result.columns + ) assert len(peek_result) == 3 @@ -878,7 +886,7 @@ def test_df_peek_exception(scalars_dfs): def test_df_peek_force_default(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs peek_result = scalars_df[["int64_col", "int64_too"]].cumsum().peek(n=3) - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( scalars_pandas_df[["int64_col", "int64_too"]].columns, peek_result.columns ) assert len(peek_result) == 3 @@ -889,7 +897,7 @@ def test_df_peek_reset_index(scalars_dfs): peek_result = ( scalars_df[["int64_col", "int64_too"]].reset_index(drop=True).peek(n=3) ) - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( scalars_pandas_df[["int64_col", "int64_too"]].columns, peek_result.columns ) assert len(peek_result) == 3 @@ -989,7 +997,7 @@ def test_df_column_name_with_space(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"bool_col": "bool col"} df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -998,7 +1006,7 @@ def test_df_column_name_duplicate(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs col_name_dict = {"int64_too": "int64_col"} df_pandas = scalars_df.rename(columns=col_name_dict).to_pandas() - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df_pandas.columns, scalars_pandas_df.rename(columns=col_name_dict).columns ) @@ -1009,7 +1017,7 @@ def test_get_df_column_name_duplicate(scalars_dfs): bf_result = scalars_df.rename(columns=col_name_dict)["int64_col"].to_pandas() pd_result = scalars_pandas_df.rename(columns=col_name_dict)["int64_col"] - bigframes.testing.assert_index_equal(bf_result.columns, pd_result.columns) + bigframes.testing.utils.assert_index_equal(bf_result.columns, pd_result.columns) @pytest.mark.parametrize( @@ -1126,7 +1134,7 @@ def test_assign_new_column_w_loc(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -1148,7 +1156,7 @@ def test_assign_new_column_w_setitem(scalars_dfs, scalar): # Convert default pandas dtypes `float64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Float64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_new_column_w_setitem_dataframe(scalars_dfs): @@ -1161,7 +1169,7 @@ def test_assign_new_column_w_setitem_dataframe(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_df["int64_col"] = pd_df["int64_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df) def test_assign_new_column_w_setitem_dataframe_error(scalars_dfs): @@ -1187,7 +1195,7 @@ def test_assign_new_column_w_setitem_list(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_new_column_w_setitem_list_repeated(scalars_dfs): @@ -1205,7 +1213,7 @@ def test_assign_new_column_w_setitem_list_repeated(scalars_dfs): pd_result["new_col"] = pd_result["new_col"].astype("Int64") pd_result["new_col_2"] = pd_result["new_col_2"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_new_column_w_setitem_list_custom_index(scalars_dfs): @@ -1225,7 +1233,7 @@ def test_assign_new_column_w_setitem_list_custom_index(scalars_dfs): # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["new_col"] = pd_result["new_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_new_column_w_setitem_list_error(scalars_dfs): @@ -1267,7 +1275,7 @@ def test_setitem_multicolumn_with_literals(scalars_dfs, key, value): bf_result[key] = value pd_result[key] = value - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result.to_pandas(), check_dtype=False ) @@ -1288,7 +1296,7 @@ def test_setitem_multicolumn_with_dataframes(scalars_dfs): bf_result[["int64_col", "int64_too"]] = bf_result[["int64_too", "int64_col"]] / 2 pd_result[["int64_col", "int64_too"]] = pd_result[["int64_too", "int64_col"]] / 2 - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result.to_pandas(), check_dtype=False ) @@ -1445,7 +1453,7 @@ def test_assign_different_df_w_loc( # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_different_df_w_setitem( @@ -1464,7 +1472,7 @@ def test_assign_different_df_w_setitem( # Convert default pandas dtypes `int64` to match BigQuery DataFrames dtypes. pd_result["int64_col"] = pd_result["int64_col"].astype("Int64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_assign_callable_lambda(scalars_dfs): @@ -1534,7 +1542,7 @@ def test_df_dropna_by_thresh(scalars_dfs, axis, ignore_index, subset, thresh): bf_result = df_result.to_pandas() # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_df_dropna_range_columns(scalars_dfs): @@ -1582,7 +1590,7 @@ def test_df_fillna(scalars_dfs, col, fill_value): bf_result = scalars_df[col].fillna(fill_value).to_pandas() pd_result = scalars_pandas_df[col].fillna(fill_value) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) def test_df_replace_scalar_scalar(scalars_dfs): @@ -1591,7 +1599,7 @@ def test_df_replace_scalar_scalar(scalars_dfs): pd_result = scalars_pandas_df.replace(555.555, 3) # pandas has narrower result types as they are determined dynamically - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) def test_df_replace_regex_scalar(scalars_dfs): @@ -1599,7 +1607,7 @@ def test_df_replace_regex_scalar(scalars_dfs): bf_result = scalars_df.replace("^H.l", "Howdy, Planet!", regex=True).to_pandas() pd_result = scalars_pandas_df.replace("^H.l", "Howdy, Planet!", regex=True) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -1611,7 +1619,7 @@ def test_df_replace_list_scalar(scalars_dfs): pd_result = scalars_pandas_df.replace([555.555, 3.2], 3) # pandas has narrower result types as they are determined dynamically - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, @@ -1623,7 +1631,7 @@ def test_df_replace_value_dict(scalars_dfs): bf_result = scalars_df.replace(1, {"int64_col": 100, "int64_too": 200}).to_pandas() pd_result = scalars_pandas_df.replace(1, {"int64_col": 100, "int64_too": 200}) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -1840,7 +1848,9 @@ def test_df_cross_merge(scalars_dfs): ), "cross", ) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -1993,7 +2003,9 @@ def test_self_merge_self_w_on_args(): bf_result = bf_df1.merge( bf_df2, left_on=["A", "C"], right_on=["B", "C"], how="inner" ).to_pandas() - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -2034,7 +2046,7 @@ def test_get_dtypes(scalars_df_default_index): "timestamp_col": pd.ArrowDtype(pa.timestamp("us", tz="UTC")), "duration_col": pd.ArrowDtype(pa.duration("us")), } - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( dtypes, pd.Series(dtypes_dict), ) @@ -2050,7 +2062,7 @@ def test_get_dtypes_array_struct_query(session): ) dtypes = df.dtypes - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( dtypes, pd.Series( { @@ -2070,7 +2082,7 @@ def test_get_dtypes_array_struct_query(session): def test_get_dtypes_array_struct_table(nested_df): dtypes = nested_df.dtypes - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( dtypes, pd.Series( { @@ -2608,7 +2620,7 @@ def test_combine( ) # Some dtype inconsistency for all-NULL columns - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -2646,7 +2658,7 @@ def test_df_update(overwrite, filter_func): bf_df1.update(bf_df2, overwrite=overwrite, filter_func=filter_func) pd_df1.update(pd_df2, overwrite=overwrite, filter_func=filter_func) - bigframes.testing.assert_frame_equal(bf_df1.to_pandas(), pd_df1) + bigframes.testing.utils.assert_frame_equal(bf_df1.to_pandas(), pd_df1) def test_df_idxmin(): @@ -2658,7 +2670,7 @@ def test_df_idxmin(): bf_result = bf_df.idxmin().to_pandas() pd_result = pd_df.idxmin() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_dtype=False ) @@ -2672,7 +2684,7 @@ def test_df_idxmax(): bf_result = bf_df.idxmax().to_pandas() pd_result = pd_df.idxmax() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_dtype=False ) @@ -2712,10 +2724,10 @@ def test_df_align(join, axis): assert isinstance(bf_result1, dataframe.DataFrame) and isinstance( bf_result2, dataframe.DataFrame ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result1.to_pandas(), pd_result1, check_dtype=False ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result2.to_pandas(), pd_result2, check_dtype=False ) @@ -2742,7 +2754,7 @@ def test_combine_first( pd_result = pd_df_a.combine_first(pd_df_b) # Some dtype inconsistency for all-NULL columns - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -2769,9 +2781,9 @@ def test_df_corr_w_numeric_only(scalars_dfs_maybe_ordered, columns, numeric_only # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_index_equal(bf_result.columns, pd_result.columns) + bigframes.testing.utils.assert_index_equal(bf_result.columns, pd_result.columns) # Only check row order in ordered mode. - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, @@ -2813,9 +2825,9 @@ def test_cov_w_numeric_only(scalars_dfs_maybe_ordered, columns, numeric_only): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_index_equal(bf_result.columns, pd_result.columns) + bigframes.testing.utils.assert_index_equal(bf_result.columns, pd_result.columns) # Only check row order in ordered mode. - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, @@ -2836,7 +2848,7 @@ def test_df_corrwith_df(scalars_dfs_maybe_ordered): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -2857,7 +2869,7 @@ def test_df_corrwith_df_numeric_only(scalars_dfs): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -2886,7 +2898,7 @@ def test_df_corrwith_series(scalars_dfs_maybe_ordered): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -3157,7 +3169,7 @@ def test_binop_df_df_binary_op( pd_result = pd_df_a - pd_df_b # Some dtype inconsistency for all-NULL columns - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) # Differnt table will only work for explicit index, since default index orders are arbitrary. @@ -3267,9 +3279,11 @@ def test_join_different_table_with_duplicate_column_name( pd_result = pd_df_a.join(pd_df_b, how=how, lsuffix="_l", rsuffix="_r") # Ensure no inplace changes - bigframes.testing.assert_index_equal(bf_df_a.columns, pd_df_a.columns) - bigframes.testing.assert_index_equal(bf_df_b.index.to_pandas(), pd_df_b.index) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_index_equal(bf_df_a.columns, pd_df_a.columns) + bigframes.testing.utils.assert_index_equal(bf_df_b.index.to_pandas(), pd_df_b.index) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) @all_joins @@ -3297,14 +3311,14 @@ def test_join_param_on_with_duplicate_column_name_not_on_col( pd_result = pd_df_a.join( pd_df_b, on="int64_too", how=how, lsuffix="_l", rsuffix="_r" ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.sort_index(), pd_result.sort_index(), check_like=True, check_index_type=False, check_names=False, ) - bigframes.testing.assert_index_equal(bf_result.columns, pd_result.columns) + bigframes.testing.utils.assert_index_equal(bf_result.columns, pd_result.columns) @pytest.mark.skipif( @@ -3335,14 +3349,14 @@ def test_join_param_on_with_duplicate_column_name_on_col( pd_result = pd_df_a.join( pd_df_b, on="int64_too", how=how, lsuffix="_l", rsuffix="_r" ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.sort_index(), pd_result.sort_index(), check_like=True, check_index_type=False, check_names=False, ) - bigframes.testing.assert_index_equal(bf_result.columns, pd_result.columns) + bigframes.testing.utils.assert_index_equal(bf_result.columns, pd_result.columns) @all_joins @@ -3487,7 +3501,7 @@ def test_dataframe_numeric_analytic_op( bf_series = operator(scalars_df_index[columns]) pd_series = operator(scalars_pandas_df_index[columns]) bf_result = bf_series.to_pandas() - bigframes.testing.assert_frame_equal(pd_series, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_series, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -3512,7 +3526,7 @@ def test_dataframe_general_analytic_op( bf_series = operator(scalars_df_index[col_names]) pd_series = operator(scalars_pandas_df_index[col_names]) bf_result = bf_series.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_series, bf_result, ) @@ -3530,7 +3544,7 @@ def test_dataframe_diff(scalars_df_index, scalars_pandas_df_index, periods): col_names = ["int64_too", "float64_col", "int64_col"] bf_result = scalars_df_index[col_names].diff(periods=periods).to_pandas() pd_result = scalars_pandas_df_index[col_names].diff(periods=periods) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -3549,7 +3563,7 @@ def test_dataframe_pct_change(scalars_df_index, scalars_pandas_df_index, periods bf_result = scalars_df_index[col_names].pct_change(periods=periods).to_pandas() # pandas 3.0 does not automatically ffill anymore pd_result = scalars_pandas_df_index[col_names].ffill().pct_change(periods=periods) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -3563,7 +3577,7 @@ def test_dataframe_agg_single_string(scalars_dfs): pd_result = scalars_pandas_df[numeric_cols].agg("sum") assert bf_result.dtype == "Float64" - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -3583,7 +3597,7 @@ def test_dataframe_agg_int_single_string(scalars_dfs, agg): pd_result = scalars_pandas_df[numeric_cols].agg(agg) assert bf_result.dtype == "Int64" - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -3638,7 +3652,7 @@ def test_dataframe_agg_int_multi_string(scalars_dfs): # Pandas may produce narrower numeric types # Pandas has object index type - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -3681,7 +3695,7 @@ def test_df_transpose_repeated_uses_cache(): bf_df = bf_df.transpose() + i pd_df = pd_df.transpose() + i - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_df, bf_df.to_pandas(), check_dtype=False, check_index_type=False ) @@ -3724,7 +3738,7 @@ def test_df_melt_default(scalars_dfs): pd_result = scalars_pandas_df[columns].melt() # Pandas produces int64 index, Bigframes produces Int64 (nullable) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -3753,7 +3767,7 @@ def test_df_melt_parameterized(scalars_dfs): ) # Pandas produces int64 index, Bigframes produces Int64 (nullable) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, check_dtype=False ) @@ -3806,7 +3820,7 @@ def test_df_pivot(scalars_dfs, values, index, columns): # Pandas produces NaN, where bq dataframes produces pd.NA bf_result = bf_result.fillna(float("nan")) pd_result = pd_result.fillna(float("nan")) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -3827,7 +3841,7 @@ def test_df_pivot_hockey(hockey_df, hockey_pandas_df, values, index, columns): ) # Pandas produces NaN, where bq dataframes produces pd.NA - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -3868,7 +3882,7 @@ def test_df_pivot_table( aggfunc=aggfunc, fill_value=fill_value, ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_column_type=False ) @@ -3948,7 +3962,7 @@ def test__dir__with_rename(scalars_dfs): def test_loc_select_columns_w_repeats(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[["int64_col", "int64_col", "int64_too"]].to_pandas() pd_result = scalars_pandas_df_index[["int64_col", "int64_col", "int64_too"]] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -3972,7 +3986,7 @@ def test_loc_select_columns_w_repeats(scalars_df_index, scalars_pandas_df_index) def test_iloc_slice(scalars_df_index, scalars_pandas_df_index, start, stop, step): bf_result = scalars_df_index.iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index.iloc[start:stop:step] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -3990,7 +4004,7 @@ def test_iloc_slice_after_cache( scalars_df_index.cache() bf_result = scalars_df_index.iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index.iloc[start:stop:step] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4023,7 +4037,7 @@ def test_iloc_single_integer(scalars_df_index, scalars_pandas_df_index, index): bf_result = scalars_df_index.iloc[index] pd_result = scalars_pandas_df_index.iloc[index] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -4048,14 +4062,14 @@ def test_iloc_tuple_multi_columns(scalars_df_index, scalars_pandas_df_index, ind bf_result = scalars_df_index.iloc[index].to_pandas() pd_result = scalars_pandas_df_index.iloc[index] - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_iloc_tuple_multi_columns_single_row(scalars_df_index, scalars_pandas_df_index): index = (2, [2, 1, 3, -4]) bf_result = scalars_df_index.iloc[index] pd_result = scalars_pandas_df_index.iloc[index] - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -4109,7 +4123,7 @@ def test_loc_bool_series(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[scalars_df_index.bool_col].to_pandas() pd_result = scalars_pandas_df_index.loc[scalars_pandas_df_index.bool_col] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4120,7 +4134,7 @@ def test_loc_list_select_rows_and_columns(scalars_df_index, scalars_pandas_df_in bf_result = scalars_df_index.loc[idx_list, ["bool_col", "int64_col"]].to_pandas() pd_result = scalars_pandas_df_index.loc[idx_list, ["bool_col", "int64_col"]] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4129,7 +4143,7 @@ def test_loc_list_select_rows_and_columns(scalars_df_index, scalars_pandas_df_in def test_loc_select_column(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[:, "int64_col"].to_pandas() pd_result = scalars_pandas_df_index.loc[:, "int64_col"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -4140,7 +4154,7 @@ def test_loc_select_with_column_condition(scalars_df_index, scalars_pandas_df_in pd_result = scalars_pandas_df_index.loc[ :, scalars_pandas_df_index.dtypes == "Int64" ] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4163,7 +4177,7 @@ def test_loc_select_with_column_condition_bf_series( pd_result = scalars_pandas_df_index.loc[ :, scalars_pandas_df_index.nunique() > size_half ] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4177,7 +4191,7 @@ def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_ind index = "Hello, World!" bf_result = scalars_df_index.loc[index] pd_result = scalars_pandas_df_index.loc[index] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -4189,7 +4203,7 @@ def test_loc_single_index_no_duplicate(scalars_df_index, scalars_pandas_df_index index = -2345 bf_result = scalars_df_index.loc[index] pd_result = scalars_pandas_df_index.loc[index] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -4203,7 +4217,7 @@ def test_at_with_duplicate(scalars_df_index, scalars_pandas_df_index): index = "Hello, World!" bf_result = scalars_df_index.at[index, "int64_too"] pd_result = scalars_pandas_df_index.at[index, "int64_too"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4228,7 +4242,7 @@ def test_loc_setitem_bool_series_scalar_new_col(scalars_dfs): # pandas uses float64 instead pd_df["new_col"] = pd_df["new_col"].astype("Float64") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_df.to_pandas(), pd_df, ) @@ -4252,7 +4266,7 @@ def test_loc_setitem_bool_series_scalar_existing_col(scalars_dfs, col, value): bf_df.loc[bf_df["int64_too"] == 1, col] = value pd_df.loc[pd_df["int64_too"] == 1, col] = value - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_df.to_pandas(), pd_df, ) @@ -4405,7 +4419,9 @@ def test_dataframe_aggregates_quantile_mono(scalars_df_index, scalars_pandas_df_ # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_dataframe_aggregates_quantile_multi(scalars_df_index, scalars_pandas_df_index): @@ -4418,7 +4434,7 @@ def test_dataframe_aggregates_quantile_multi(scalars_df_index, scalars_pandas_df pd_result = pd_result.astype("Float64") pd_result.index = pd_result.index.astype("Float64") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -4444,7 +4460,9 @@ def test_dataframe_bool_aggregates(scalars_df_index, scalars_pandas_df_index, op bf_result = bf_series.to_pandas() pd_series.index = pd_series.index.astype(bf_result.index.dtype) - bigframes.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_series, bf_result, check_index_type=False + ) def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): @@ -4456,7 +4474,9 @@ def test_dataframe_prod(scalars_df_index, scalars_pandas_df_index): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_series = pd_series.astype("Float64") # Pandas has object index type - bigframes.testing.assert_series_equal(pd_series, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_series, bf_result, check_index_type=False + ) def test_df_skew_too_few_values(scalars_dfs): @@ -4468,7 +4488,9 @@ def test_df_skew_too_few_values(scalars_dfs): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -4501,7 +4523,9 @@ def test_df_kurt_too_few_values(scalars_dfs): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_index_type=False + ) def test_df_kurt(scalars_dfs): @@ -4513,7 +4537,9 @@ def test_df_kurt(scalars_dfs): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -4597,7 +4623,7 @@ def test_df_add_prefix(scalars_df_index, scalars_pandas_df_index, axis): pd_result = scalars_pandas_df_index.add_prefix("prefix_", axis) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -4618,7 +4644,7 @@ def test_df_add_suffix(scalars_df_index, scalars_pandas_df_index, axis): pd_result = scalars_pandas_df_index.add_suffix("_suffix", axis) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -4638,7 +4664,7 @@ def test_df_columns_filter_items(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.filter(items=["string_col", "int64_col"]) # Ignore column ordering as pandas order differently depending on version - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.sort_index(axis=1), pd_result.sort_index(axis=1), ) @@ -4649,7 +4675,7 @@ def test_df_columns_filter_like(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.filter(like="64_col") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4660,7 +4686,7 @@ def test_df_columns_filter_regex(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.filter(regex="^[^_]+$") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4692,7 +4718,7 @@ def test_df_rows_filter_like(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.filter(like="ello", axis=0) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4706,7 +4732,7 @@ def test_df_rows_filter_regex(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.filter(regex="^[GH].*", axis=0) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4737,7 +4763,7 @@ def test_df_reindex_rows_index(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4762,7 +4788,7 @@ def test_df_reindex_columns(scalars_df_index, scalars_pandas_df_index): # Pandas uses float64 as default for newly created empty column, bf uses Float64 pd_result.not_a_col = pd_result.not_a_col.astype(pandas.Float64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4777,7 +4803,7 @@ def test_df_reindex_columns_with_same_order(scalars_df_index, scalars_pandas_df_ bf_result = bf.reindex(columns=columns).to_pandas() pd_result = pd_df.reindex(columns=columns) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4867,7 +4893,7 @@ def test_df_reindex_like(scalars_df_index, scalars_pandas_df_index): pd_result.index = pd_result.index.astype(pd.Int64Dtype()) # Pandas uses float64 as default for newly created empty column, bf uses Float64 pd_result.not_a_col = pd_result.not_a_col.astype(pandas.Float64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -4878,7 +4904,7 @@ def test_df_values(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.values # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False ) @@ -4888,7 +4914,7 @@ def test_df_to_numpy(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.to_numpy() # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False ) @@ -4898,7 +4924,7 @@ def test_df___array__(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.__array__() # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd.DataFrame(bf_result), pd.DataFrame(pd_result), check_dtype=False ) @@ -4990,7 +5016,7 @@ def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[index_list].to_pandas() pd_result = scalars_pandas_df_index.loc[index_list] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -5002,7 +5028,7 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[index_list] pd_result = scalars_pandas_df_index.loc[index_list] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5037,7 +5063,7 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index, index_list): bf_result = scalars_df_index.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5057,7 +5083,7 @@ def test_iloc_list_partial_ordering( bf_result = scalars_df_partial_ordering.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5075,7 +5101,7 @@ def test_iloc_list_multiindex(scalars_dfs): bf_result = scalars_df.iloc[index_list] pd_result = scalars_pandas_df.iloc[index_list] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5095,7 +5121,7 @@ def test_rename_axis(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.rename_axis("newindexname") pd_result = scalars_pandas_df_index.rename_axis("newindexname") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5105,7 +5131,7 @@ def test_rename_axis_nonstring(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.rename_axis((4,)) pd_result = scalars_pandas_df_index.rename_axis((4,)) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5121,7 +5147,7 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[bf_string_series] pd_result = scalars_pandas_df_index.loc[pd_string_series] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5139,7 +5165,7 @@ def test_loc_bf_series_multiindex(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_multiindex.loc[bf_string_series] pd_result = scalars_pandas_df_multiindex.loc[pd_string_series] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5152,7 +5178,7 @@ def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.loc[bf_index] pd_result = scalars_pandas_df_index.loc[pd_index] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5172,7 +5198,7 @@ def test_loc_bf_index_integer_index_renamed_col( bf_result = scalars_df_index.loc[bf_index] pd_result = scalars_pandas_df_index.loc[pd_index] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.to_pandas(), pd_result, ) @@ -5198,7 +5224,7 @@ def test_df_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, sub columns = ["bool_col", "int64_too", "int64_col"] bf_df = scalars_df_index[columns].drop_duplicates(subset, keep=keep).to_pandas() pd_df = scalars_pandas_df_index[columns].drop_duplicates(subset, keep=keep) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_df, bf_df, ) @@ -5225,7 +5251,7 @@ def test_df_drop_duplicates_w_json(json_df, keep): pd_df = json_pandas_df.drop_duplicates(keep=keep) pd_df["json_col"] = pd_df["json_col"].astype(dtypes.JSON_DTYPE) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_df, bf_df, ) @@ -5250,7 +5276,7 @@ def test_df_duplicated(scalars_df_index, scalars_pandas_df_index, keep, subset): columns = ["bool_col", "int64_too", "int64_col"] bf_series = scalars_df_index[columns].duplicated(subset, keep=keep).to_pandas() pd_series = scalars_pandas_df_index[columns].duplicated(subset, keep=keep) - bigframes.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_series, bf_series, check_dtype=False) def test_df_from_dict_columns_orient(): @@ -5479,7 +5505,7 @@ def test_df_eval(scalars_dfs, expr): bf_result = scalars_df.eval(expr).to_pandas() pd_result = scalars_pandas_df.eval(expr) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -5500,7 +5526,7 @@ def test_df_query(scalars_dfs, expr): bf_result = scalars_df.query(expr).to_pandas() pd_result = scalars_pandas_df.query(expr) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -5525,7 +5551,7 @@ def test_df_value_counts(scalars_dfs, subset, normalize, ascending, dropna): subset, normalize=normalize, ascending=ascending, dropna=dropna ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, @@ -5577,7 +5603,7 @@ def test_df_rank_with_nulls( .astype(pd.Float64Dtype()) ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -5674,7 +5700,7 @@ def test_df_dot_inline(session): pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -5691,7 +5717,7 @@ def test_df_dot( for name in pd_result.columns: pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -5708,7 +5734,7 @@ def test_df_dot_operator( for name in pd_result.columns: pd_result[name] = pd_result[name].astype(pd.Int64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -5731,7 +5757,7 @@ def test_df_dot_series_inline(): pd_result = pd_result.astype(pd.Int64Dtype()) pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -5747,7 +5773,7 @@ def test_df_dot_series( # Pandas result is object instead of Int64 (nullable) dtype. pd_result = pd_result.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -5763,7 +5789,7 @@ def test_df_dot_operator_series( # Pandas result is object instead of Int64 (nullable) dtype. pd_result = pd_result.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -5905,7 +5931,7 @@ def test_dataframe_explode(col_names, ignore_index, session): bf_materialized = bf_result.to_pandas() execs_post = metrics.execution_count - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_materialized, pd_result, check_index_type=False, @@ -5936,7 +5962,7 @@ def test_dataframe_explode_reserve_order(ignore_index, ordered): pd_res = pd_df.explode(["a", "b"], ignore_index=ignore_index).astype( pd.Int64Dtype() ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( res if ordered else res.sort_index(), pd_res, check_index_type=False, @@ -5990,7 +6016,7 @@ def test_resample_with_column( ].max() # TODO: (b/484364312) pd_result.index.names = bf_result.index.names - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6106,7 +6132,7 @@ def test_resample_start_time(rule, origin, data): # TODO: (b/484364312) pd_result.index.names = bf_result.index.names - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6131,7 +6157,9 @@ def test_df_astype(scalars_dfs, dtype): bf_result = bf_df.astype(dtype).to_pandas() pd_result = pd_df.astype(dtype) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) def test_df_astype_python_types(scalars_dfs): @@ -6145,7 +6173,9 @@ def test_df_astype_python_types(scalars_dfs): {"bool_col": "string[pyarrow]", "int64_col": pd.Float64Dtype()} ) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) def test_astype_invalid_type_fail(scalars_dfs): @@ -6165,7 +6195,7 @@ def test_agg_with_dict_lists_strings(scalars_dfs): bf_result = bf_df.agg(agg_funcs).to_pandas() pd_result = pd_df.agg(agg_funcs) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6185,7 +6215,7 @@ def test_agg_with_dict_lists_callables(scalars_dfs): bf_result = bf_df.agg(agg_funcs).to_pandas() pd_result = pd_df.agg(agg_funcs) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6200,7 +6230,7 @@ def test_agg_with_dict_list_and_str(scalars_dfs): bf_result = bf_df.agg(agg_funcs).to_pandas() pd_result = pd_df.agg(agg_funcs) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6217,7 +6247,7 @@ def test_agg_with_dict_strs(scalars_dfs): pd_result = pd_df.agg(agg_funcs) pd_result.index = pd_result.index.astype("string[pyarrow]") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -6239,7 +6269,7 @@ def test_df_agg_with_builtins(scalars_dfs): .agg({"int64_col": [len, sum, min, max, list], "bool_col": [all, any, max]}) ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index cce230ae17d..fece679d061 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -63,7 +63,7 @@ def test_sql_executes(scalars_df_default_index, bigquery_client): .reset_index(drop=True) ) bq_result["bytes_col"] = bq_result["bytes_col"].astype(dtypes.BYTES_DTYPE) - bigframes.testing.assert_frame_equal(bf_result, bq_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, bq_result, check_dtype=False) def test_sql_executes_and_includes_named_index( @@ -95,7 +95,7 @@ def test_sql_executes_and_includes_named_index( .sort_values("rowindex") ) bq_result["bytes_col"] = bq_result["bytes_col"].astype(dtypes.BYTES_DTYPE) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, bq_result, check_dtype=False, check_index_type=False ) @@ -129,7 +129,7 @@ def test_sql_executes_and_includes_named_multiindex( .sort_values("rowindex") ) bq_result["bytes_col"] = bq_result["bytes_col"].astype(dtypes.BYTES_DTYPE) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, bq_result, check_dtype=False, check_index_type=False ) @@ -367,7 +367,7 @@ def test_to_pandas_batches_w_empty_dataframe(session): assert len(results) == 1 assert list(results[0].index.names) == ["idx1", "idx2"] assert list(results[0].columns) == ["col1", "col2"] - bigframes.testing.assert_series_equal(results[0].dtypes, empty.dtypes) + bigframes.testing.utils.assert_series_equal(results[0].dtypes, empty.dtypes) @pytest.mark.skipif( diff --git a/tests/system/small/test_groupby.py b/tests/system/small/test_groupby.py index b6c87091915..d488b0a5adb 100644 --- a/tests/system/small/test_groupby.py +++ b/tests/system/small/test_groupby.py @@ -17,7 +17,7 @@ import pytest import bigframes.pandas as bpd -import bigframes.testing +import bigframes.testing.utils # ================= # DataFrame.groupby @@ -51,7 +51,7 @@ def test_dataframe_groupby_numeric_aggregate( pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) bf_result_computed = bf_result.to_pandas() # Pandas std function produces float64, not matching Float64 from bigframes - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -60,7 +60,7 @@ def test_dataframe_groupby_head(scalars_df_index, scalars_pandas_df_index): col_names = ["int64_too", "float64_col", "int64_col", "bool_col", "string_col"] bf_result = scalars_df_index[col_names].groupby("bool_col").head(2).to_pandas() pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").head(2) - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) def test_dataframe_groupby_len(scalars_df_index, scalars_pandas_df_index): @@ -101,7 +101,7 @@ def test_dataframe_groupby_quantile(scalars_df_index, scalars_pandas_df_index, q scalars_df_index[col_names].groupby("string_col").quantile(q) ).to_pandas() pd_result = scalars_pandas_df_index[col_names].groupby("string_col").quantile(q) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -141,7 +141,7 @@ def test_dataframe_groupby_rank( .astype("float64") .astype("Float64") ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -169,7 +169,7 @@ def test_dataframe_groupby_aggregate( pd_result = operator(scalars_pandas_df_index[col_names].groupby("string_col")) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -179,7 +179,7 @@ def test_dataframe_groupby_corr(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_names].groupby("bool_col").corr().to_pandas() pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").corr() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -189,7 +189,7 @@ def test_dataframe_groupby_cov(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_names].groupby("bool_col").cov().to_pandas() pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").cov() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -209,7 +209,7 @@ def test_dataframe_groupby_agg_string( pd_result = scalars_pandas_df_index[col_names].groupby("string_col").agg("count") bf_result_computed = bf_result.to_pandas(ordered=ordered) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, ignore_order=not ordered ) @@ -219,7 +219,7 @@ def test_dataframe_groupby_agg_size_string(scalars_df_index, scalars_pandas_df_i bf_result = scalars_df_index[col_names].groupby("string_col").agg("size") pd_result = scalars_pandas_df_index[col_names].groupby("string_col").agg("size") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result.to_pandas(), check_dtype=False ) @@ -239,7 +239,7 @@ def test_dataframe_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): # some inconsistency between versions, so normalize to bigframes behavior pd_result = pd_result.rename({"amin": "min"}, axis="columns") bf_result_computed = bf_result_computed.rename({"amin": "min"}, axis="columns") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, check_index_type=False ) @@ -258,7 +258,7 @@ def test_dataframe_groupby_agg_list_w_column_multi_index( pd_result = pd_df.groupby(level=0).agg(["count", np.min, "size"]) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -290,7 +290,7 @@ def test_dataframe_groupby_agg_dict_with_list( ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, check_index_type=False ) @@ -309,7 +309,7 @@ def test_dataframe_groupby_agg_dict_no_lists(scalars_df_index, scalars_pandas_df ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -334,7 +334,7 @@ def test_dataframe_groupby_agg_named(scalars_df_index, scalars_pandas_df_index): ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -356,7 +356,7 @@ def test_dataframe_groupby_agg_kw_tuples(scalars_df_index, scalars_pandas_df_ind ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -403,7 +403,7 @@ def test_dataframe_groupby_multi_sum( # BigQuery DataFrames default indices use nullable Int64 always pd_series.index = pd_series.index.astype("Int64") - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_series, bf_result, ) @@ -442,7 +442,7 @@ def test_dataframe_groupby_analytic( ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -465,7 +465,7 @@ def test_dataframe_groupby_cumcount( ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -477,7 +477,7 @@ def test_dataframe_groupby_size_as_index_false( bf_result_computed = bf_result.to_pandas() pd_result = scalars_pandas_df_index.groupby("string_col", as_index=False).size() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, check_index_type=False ) @@ -489,7 +489,7 @@ def test_dataframe_groupby_size_as_index_true( pd_result = scalars_pandas_df_index.groupby("string_col", as_index=True).size() bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -499,7 +499,7 @@ def test_dataframe_groupby_skew(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_names].groupby("bool_col").skew().to_pandas() pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").skew() - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.skipif( @@ -512,7 +512,7 @@ def test_dataframe_groupby_kurt(scalars_df_index, scalars_pandas_df_index): # Pandas doesn't have groupby.kurt yet: https://github.com/pandas-dev/pandas/issues/40139 pd_result = scalars_pandas_df_index[col_names].groupby("bool_col").kurt() - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -528,7 +528,7 @@ def test_dataframe_groupby_diff(scalars_df_index, scalars_pandas_df_index, order pd_result = scalars_pandas_df_index[col_names].groupby("string_col").diff(-1) bf_result_computed = bf_result.to_pandas(ordered=ordered) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, ignore_order=not ordered ) @@ -545,7 +545,7 @@ def test_dataframe_groupby_getitem( scalars_pandas_df_index[col_names].groupby("string_col")["int64_col"].min() ) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) def test_dataframe_groupby_getitem_error( @@ -576,7 +576,7 @@ def test_dataframe_groupby_getitem_list( scalars_pandas_df_index[col_names].groupby("string_col")[col_names].min() ) - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) def test_dataframe_groupby_getitem_list_error( @@ -609,7 +609,7 @@ def test_dataframe_groupby_nonnumeric_with_mean(): bf_result = bpd.DataFrame(df).groupby(["key1", "key2"]).mean().to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, check_index_type=False, check_dtype=False ) @@ -654,10 +654,14 @@ def test_dataframe_groupby_value_counts( ) if as_index: - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_dtype=False + ) else: pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal( + pd_result, bf_result, check_dtype=False + ) @pytest.mark.parametrize( @@ -683,7 +687,7 @@ def test_dataframe_groupby_first( .groupby(scalars_pandas_df_index.int64_col % 2) .first(numeric_only=numeric_only, min_count=min_count) ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -707,7 +711,7 @@ def test_dataframe_groupby_last( pd_result = scalars_pandas_df_index.groupby( scalars_pandas_df_index.int64_col % 2 ).last(numeric_only=numeric_only, min_count=min_count) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result, ) @@ -743,7 +747,7 @@ def test_series_groupby_agg_string(scalars_df_index, scalars_pandas_df_index, ag ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result_computed, check_dtype=False, check_names=False ) @@ -761,7 +765,7 @@ def test_series_groupby_agg_list(scalars_df_index, scalars_pandas_df_index): ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result, bf_result_computed, check_dtype=False, check_names=False ) @@ -816,7 +820,7 @@ def test_series_groupby_rank( .astype("float64") .astype("Float64") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -831,7 +835,7 @@ def test_series_groupby_head(scalars_df_index, scalars_pandas_df_index, dropna): pd_result = scalars_pandas_df_index.groupby("bool_col", dropna=dropna)[ "int64_too" ].head(1) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): @@ -846,7 +850,7 @@ def test_series_groupby_kurt(scalars_df_index, scalars_pandas_df_index): pd.Series.kurt ) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) def test_series_groupby_size(scalars_df_index, scalars_pandas_df_index): @@ -860,7 +864,7 @@ def test_series_groupby_size(scalars_df_index, scalars_pandas_df_index): ) bf_result_computed = bf_result.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result_computed, check_dtype=False ) @@ -878,7 +882,7 @@ def test_series_groupby_skew(scalars_df_index, scalars_pandas_df_index): .skew() ) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -893,7 +897,7 @@ def test_series_groupby_quantile(scalars_df_index, scalars_pandas_df_index, q): scalars_df_index.groupby("string_col")["int64_col"].quantile(q) ).to_pandas() pd_result = scalars_pandas_df_index.groupby("string_col")["int64_col"].quantile(q) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, check_dtype=False, check_index_type=False ) @@ -934,7 +938,7 @@ def test_series_groupby_value_counts( pd_result = scalars_pandas_df_index.groupby("bool_col")["string_col"].value_counts( normalize=normalize, ascending=ascending, dropna=dropna ) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -955,7 +959,7 @@ def test_series_groupby_first( pd_result = scalars_pandas_df_index.groupby("string_col")["int64_col"].first( numeric_only=numeric_only, min_count=min_count ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -979,4 +983,4 @@ def test_series_groupby_last( pd_result = scalars_pandas_df_index.groupby("string_col")["int64_col"].last( numeric_only=numeric_only, min_count=min_count ) - bigframes.testing.assert_series_equal(pd_result, bf_result) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result) diff --git a/tests/system/small/test_multiindex.py b/tests/system/small/test_multiindex.py index ed901f9562e..522e8db9e45 100644 --- a/tests/system/small/test_multiindex.py +++ b/tests/system/small/test_multiindex.py @@ -17,7 +17,7 @@ import pytest import bigframes.pandas as bpd -import bigframes.testing +import bigframes.testing.utils # Sample MultiIndex for testing DataFrames where() method. _MULTI_INDEX = pandas.MultiIndex.from_tuples( @@ -58,7 +58,7 @@ def test_multi_index_from_arrays(): names=[" 1index 1", "_1index 2"], ) assert bf_idx.names == pd_idx.names - bigframes.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) + bigframes.testing.utils.assert_index_equal(bf_idx.to_pandas(), pd_idx) def test_read_pandas_multi_index_axes(): @@ -90,7 +90,7 @@ def test_read_pandas_multi_index_axes(): bf_df = bpd.DataFrame(pandas_df) bf_df_computed = bf_df.to_pandas() - bigframes.testing.assert_frame_equal(bf_df_computed, pandas_df) + bigframes.testing.utils.assert_frame_equal(bf_df_computed, pandas_df) # Row Multi-index tests @@ -98,7 +98,7 @@ def test_set_multi_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.set_index(["bool_col", "int64_too"]).to_pandas() pd_result = scalars_pandas_df_index.set_index(["bool_col", "int64_too"]) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -127,7 +127,7 @@ def test_df_reset_multi_index(scalars_df_index, scalars_pandas_df_index, level, if pd_result.index.dtype != bf_result.index.dtype: pd_result.index = pd_result.index.astype(bf_result.index.dtype) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -160,9 +160,9 @@ def test_series_reset_multi_index( pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) if drop: - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) else: - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_series_multi_index_idxmin(scalars_df_index, scalars_pandas_df_index): @@ -187,7 +187,7 @@ def test_binop_series_series_matching_multi_indices( bf_result = bf_left["int64_col"] + bf_right["int64_too"] pd_result = pd_left["int64_col"] + pd_right["int64_too"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -203,7 +203,7 @@ def test_binop_df_series_matching_multi_indices( bf_result = bf_left[["int64_col", "int64_too"]].add(bf_right["int64_too"], axis=0) pd_result = pd_left[["int64_col", "int64_too"]].add(pd_right["int64_too"], axis=0) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -217,7 +217,7 @@ def test_binop_multi_index_mono_index(scalars_df_index, scalars_pandas_df_index) bf_result = bf_left["int64_col"] + bf_right["int64_too"] pd_result = pd_left["int64_col"] + pd_right["int64_too"] - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_binop_overlapping_multi_indices(scalars_df_index, scalars_pandas_df_index): @@ -229,7 +229,7 @@ def test_binop_overlapping_multi_indices(scalars_df_index, scalars_pandas_df_ind bf_result = bf_left["int64_col"] + bf_right["int64_too"] pd_result = pd_left["int64_col"] + pd_right["int64_too"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.sort_index().to_pandas(), pd_result.sort_index() ) @@ -245,7 +245,7 @@ def test_concat_compatible_multi_indices(scalars_df_index, scalars_pandas_df_ind bf_result = bpd.concat([bf_left, bf_right]) pd_result = pandas.concat([pd_left, pd_right]) - bigframes.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result.to_pandas(), pd_result) def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_index): @@ -260,7 +260,7 @@ def test_concat_multi_indices_ignore_index(scalars_df_index, scalars_pandas_df_i # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) - bigframes.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -277,7 +277,7 @@ def test_multi_index_loc_multi_row(scalars_df_index, scalars_pandas_df_index, ke ) pd_result = scalars_pandas_df_index.set_index(["int64_too", "string_col"]).loc[key] - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_multi_index_loc_single_row(scalars_df_index, scalars_pandas_df_index): @@ -288,7 +288,7 @@ def test_multi_index_loc_single_row(scalars_df_index, scalars_pandas_df_index): (2, "capitalize, This ") ] - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): @@ -298,7 +298,7 @@ def test_multi_index_getitem_bool(scalars_df_index, scalars_pandas_df_index): bf_result = bf_frame[bf_frame["int64_col"] > 0].to_pandas() pd_result = pd_frame[pd_frame["int64_col"] > 0] - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -318,7 +318,7 @@ def test_df_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, lev bf_result = bf_frame.droplevel(level).to_pandas() pd_result = pd_frame.droplevel(level) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -338,7 +338,7 @@ def test_series_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index, bf_result = bf_frame["string_col"].droplevel(level).to_pandas() pd_result = pd_frame["string_col"].droplevel(level) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -357,7 +357,7 @@ def test_multi_index_drop(scalars_df_index, scalars_pandas_df_index, labels, lev bf_result = bf_frame.drop(labels=labels, axis="index", level=level).to_pandas() pd_result = pd_frame.drop(labels=labels, axis="index", level=level) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -382,7 +382,7 @@ def test_df_multi_index_reorder_levels( bf_result = bf_frame.reorder_levels(order).to_pandas() pd_result = pd_frame.reorder_levels(order) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -407,7 +407,7 @@ def test_series_multi_index_reorder_levels( bf_result = bf_frame["string_col"].reorder_levels(order).to_pandas() pd_result = pd_frame["string_col"].reorder_levels(order) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_df_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): @@ -417,7 +417,7 @@ def test_df_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): bf_result = bf_frame.swaplevel().to_pandas() pd_result = pd_frame.swaplevel() - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_series_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): @@ -427,7 +427,7 @@ def test_series_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index) bf_result = bf_frame["string_col"].swaplevel(0, 2).to_pandas() pd_result = pd_frame["string_col"].swaplevel(0, 2) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_multi_index_series_groupby(scalars_df_index, scalars_pandas_df_index): @@ -443,7 +443,7 @@ def test_multi_index_series_groupby(scalars_df_index, scalars_pandas_df_index): pd_frame["float64_col"].groupby([pd_frame.int64_col % 2, "bool_col"]).mean() ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -470,7 +470,7 @@ def test_multi_index_series_groupby_level( .mean() ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index): @@ -485,7 +485,7 @@ def test_multi_index_dataframe_groupby(scalars_df_index, scalars_pandas_df_index numeric_only=True ) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -521,7 +521,9 @@ def test_multi_index_dataframe_groupby_level_aggregate( bf_result = bf_result.drop(col, axis=1) # Pandas will have int64 index, while bigquery will have Int64 when resetting - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -554,7 +556,7 @@ def test_multi_index_dataframe_groupby_level_analytic( .cumsum(numeric_only=True) ) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) all_joins = pytest.mark.parametrize( @@ -584,7 +586,7 @@ def test_multi_index_dataframe_join(scalars_dfs, how): (["bool_col", "rowindex_2"]) )[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, how=how) - bigframes.testing.assert_frame_equal(bf_result, pd_result, ignore_order=True) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, ignore_order=True) @all_joins @@ -605,7 +607,7 @@ def test_multi_index_dataframe_join_on(scalars_dfs, how): pd_df_a = pd_df_a.assign(rowindex_2=pd_df_a["rowindex_2"] + 2) pd_df_b = pd_df[["float64_col"]] pd_result = pd_df_a.join(pd_df_b, on="rowindex_2", how=how) - bigframes.testing.assert_frame_equal(bf_result, pd_result, ignore_order=True) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, ignore_order=True) def test_multi_index_dataframe_where_series_cond_none_other( @@ -633,7 +635,7 @@ def test_multi_index_dataframe_where_series_cond_none_other( bf_result = dataframe_bf.where(series_cond_bf).to_pandas() pd_result = dataframe_pd.where(series_cond_pd) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -669,7 +671,7 @@ def test_multi_index_dataframe_where_series_cond_dataframe_other( bf_result = dataframe_bf.where(series_cond_bf, dataframe_other_bf).to_pandas() pd_result = dataframe_pd.where(series_cond_pd, dataframe_other_pd) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -701,7 +703,7 @@ def test_multi_index_dataframe_where_dataframe_cond_constant_other( bf_result = dataframe_bf.where(dataframe_cond_bf, other).to_pandas() pd_result = dataframe_pd.where(dataframe_cond_pd, other) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -734,7 +736,7 @@ def test_multi_index_dataframe_where_dataframe_cond_dataframe_other( bf_result = dataframe_bf.where(dataframe_cond_bf, dataframe_other_bf).to_pandas() pd_result = dataframe_pd.where(dataframe_cond_pd, dataframe_other_pd) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, @@ -766,7 +768,7 @@ def test_multi_index_series_groupby_level_aggregate( .mean() ) - bigframes.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -793,7 +795,7 @@ def test_multi_index_series_groupby_level_analytic( .cumsum() ) - bigframes.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result, check_dtype=False) def test_multi_index_series_rename_dict_same_type( @@ -808,7 +810,7 @@ def test_multi_index_series_rename_dict_same_type( "string_col" ].rename({1: 100, 2: 200}) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -826,7 +828,7 @@ def test_multi_index_df_reindex(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index.set_index(["rowindex_2", "string_col"]).reindex( index=new_index ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -844,15 +846,15 @@ def test_column_multi_index_getitem(scalars_df_index, scalars_pandas_df_index): bf_a = bf_df["a"].to_pandas() pd_a = pd_df["a"] - bigframes.testing.assert_frame_equal(bf_a, pd_a) + bigframes.testing.utils.assert_frame_equal(bf_a, pd_a) bf_b = bf_df["b"].to_pandas() pd_b = pd_df["b"] - bigframes.testing.assert_frame_equal(bf_b, pd_b) + bigframes.testing.utils.assert_frame_equal(bf_b, pd_b) bf_fullkey = bf_df[("a", "int64_too")].to_pandas() pd_fullkey = pd_df[("a", "int64_too")] - bigframes.testing.assert_series_equal(bf_fullkey, pd_fullkey) + bigframes.testing.utils.assert_series_equal(bf_fullkey, pd_fullkey) def test_column_multi_index_concat(scalars_df_index, scalars_pandas_df_index): @@ -877,7 +879,7 @@ def test_column_multi_index_concat(scalars_df_index, scalars_pandas_df_index): bf_result = bpd.concat([bf_df1, bf_df2, bf_df1]).to_pandas() pd_result = pandas.concat([pd_df1, pd_df2, pd_df1]) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_column_multi_index_drop(scalars_df_index, scalars_pandas_df_index): @@ -890,7 +892,7 @@ def test_column_multi_index_drop(scalars_df_index, scalars_pandas_df_index): bf_a = bf_df.drop(("a", "int64_too"), axis=1).to_pandas() pd_a = pd_df.drop(("a", "int64_too"), axis=1) - bigframes.testing.assert_frame_equal(bf_a, pd_a) + bigframes.testing.utils.assert_frame_equal(bf_a, pd_a) @pytest.mark.parametrize( @@ -914,7 +916,7 @@ def test_column_multi_index_assign(scalars_df_index, scalars_pandas_df_index, ke pd_result = pd_df.assign(**kwargs) # Pandas assign results in non-nullable dtype - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) def test_column_multi_index_rename(scalars_df_index, scalars_pandas_df_index): @@ -928,7 +930,7 @@ def test_column_multi_index_rename(scalars_df_index, scalars_pandas_df_index): bf_result = bf_df.rename(columns={"b": "c"}).to_pandas() pd_result = pd_df.rename(columns={"b": "c"}) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -958,7 +960,7 @@ def test_column_multi_index_reset_index( # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pandas.Int64Dtype()) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_column_multi_index_binary_op(scalars_df_index, scalars_pandas_df_index): @@ -972,7 +974,7 @@ def test_column_multi_index_binary_op(scalars_df_index, scalars_pandas_df_index) bf_result = (bf_df[("a", "a")] + 3).to_pandas() pd_result = pd_df[("a", "a")] + 3 - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_column_multi_index_any(): @@ -989,7 +991,7 @@ def test_column_multi_index_any(): pd_result = pd_df.isna().any() bf_result = bf_df.isna().any().to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result.reset_index(drop=False), pd_result.reset_index(drop=False), check_dtype=False, @@ -1009,7 +1011,9 @@ def test_column_multi_index_agg(scalars_df_index, scalars_pandas_df_index): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_frame_equal( + bf_result, pd_result, check_index_type=False + ) def test_column_multi_index_prefix_suffix(scalars_df_index, scalars_pandas_df_index): @@ -1023,7 +1027,7 @@ def test_column_multi_index_prefix_suffix(scalars_df_index, scalars_pandas_df_in bf_result = bf_df.add_prefix("prefixed_").add_suffix("_suffixed").to_pandas() pd_result = pd_df.add_prefix("prefixed_").add_suffix("_suffixed") - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_column_multi_index_cumsum(scalars_df_index, scalars_pandas_df_index): @@ -1039,7 +1043,7 @@ def test_column_multi_index_cumsum(scalars_df_index, scalars_pandas_df_index): bf_result = bf_df.cumsum().to_pandas() pd_result = pd_df.cumsum() - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -1072,7 +1076,7 @@ def test_column_multi_index_stack(level): # Pandas produces NaN, where bq dataframes produces pd.NA # Column ordering seems to depend on pandas version assert isinstance(pd_result, pandas.DataFrame) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -1100,7 +1104,7 @@ def test_column_multi_index_melt(): pd_result = pd_df.melt() # BigFrames uses different string and int types, but values are identical - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_index_type=False, check_dtype=False ) @@ -1122,7 +1126,7 @@ def test_column_multi_index_unstack(scalars_df_index, scalars_pandas_df_index): # Pandas produces NaN, where bq dataframes produces pd.NA # Column ordering seems to depend on pandas version - bigframes.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result, check_dtype=False) def test_corr_w_multi_index(scalars_df_index, scalars_pandas_df_index): @@ -1143,7 +1147,7 @@ def test_corr_w_multi_index(scalars_df_index, scalars_pandas_df_index): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses strign, Pandas uses object. - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -1166,7 +1170,7 @@ def test_cov_w_multi_index(scalars_df_index, scalars_pandas_df_index): # BigFrames and Pandas differ in their data type handling: # - Column types: BigFrames uses Float64, Pandas uses float64. # - Index types: BigFrames uses string, Pandas uses object. - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -1245,7 +1249,7 @@ def test_column_multi_index_droplevel(scalars_df_index, scalars_pandas_df_index) bf_result = bf_df.droplevel(1, axis=1).to_pandas() pd_result = pd_df.droplevel(1, axis=1) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_df_column_multi_index_reindex(scalars_df_index, scalars_pandas_df_index): @@ -1267,7 +1271,7 @@ def test_df_column_multi_index_reindex(scalars_df_index, scalars_pandas_df_index # Pandas uses float64 as default for newly created empty column, bf uses Float64 pd_result[("z", "a")] = pd_result[("z", "a")].astype(pandas.Float64Dtype()) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, ) @@ -1286,7 +1290,7 @@ def test_column_multi_index_reorder_levels(scalars_df_index, scalars_pandas_df_i bf_result = bf_df.reorder_levels([-2, -1, 0], axis=1).to_pandas() pd_result = pd_df.reorder_levels([-2, -1, 0], axis=1) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -1303,7 +1307,7 @@ def test_df_multi_index_unstack(hockey_df, hockey_pandas_df, level): ["team_name", "position"], append=True ).unstack(level=level) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -1320,7 +1324,7 @@ def test_series_multi_index_unstack(hockey_df, hockey_pandas_df, level): "number" ].unstack(level=level) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) def test_column_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index): @@ -1336,7 +1340,7 @@ def test_column_multi_index_swaplevel(scalars_df_index, scalars_pandas_df_index) bf_result = bf_df.swaplevel(-3, -1, axis=1).to_pandas() pd_result = pd_df.swaplevel(-3, -1, axis=1) - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_df_multi_index_dot_not_supported(): @@ -1410,7 +1414,7 @@ def test_explode_w_column_multi_index(): assert isinstance(pd_df, pandas.DataFrame) assert isinstance(pd_df["col0"], pandas.DataFrame) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( df["col0"].explode("col00").to_pandas(), pd_df["col0"].explode("col00"), check_dtype=False, @@ -1428,7 +1432,7 @@ def test_explode_w_multi_index(): df = bpd.DataFrame(data, index=multi_index, columns=columns) pd_df = df.to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( df.explode("col00").to_pandas(), pd_df.explode("col00"), check_dtype=False, @@ -1452,7 +1456,7 @@ def test_column_multi_index_w_na_stack(scalars_df_index, scalars_pandas_df_index # Pandas produces pd.NA, where bq dataframes produces NaN pd_result["c"] = pd_result["c"].replace(pandas.NA, np.nan) - bigframes.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, check_dtype=False) @pytest.mark.parametrize( @@ -1483,6 +1487,6 @@ def test_multiindex_eq_const(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.set_index(col_name).index == (2, False) pd_result = scalars_pandas_df_index.set_index(col_name).index == (2, False) - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( pandas.Index(pd_result, dtype="boolean"), bf_result.to_pandas() ) diff --git a/tests/system/small/test_numpy.py b/tests/system/small/test_numpy.py index d04fb81a0a2..774f72bef4a 100644 --- a/tests/system/small/test_numpy.py +++ b/tests/system/small/test_numpy.py @@ -16,7 +16,7 @@ import pandas as pd import pytest -import bigframes.testing +import bigframes.testing.utils @pytest.mark.parametrize( @@ -47,7 +47,9 @@ def test_series_ufuncs(floats_pd, floats_bf, opname): bf_result = getattr(np, opname)(floats_bf).to_pandas() pd_result = getattr(np, opname)(floats_pd) - bigframes.testing.assert_series_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, nulls_are_nan=True + ) @pytest.mark.parametrize( @@ -81,7 +83,7 @@ def test_df_ufuncs(scalars_dfs, opname): ): pd_result["int64_col"] = pd_result["int64_col"].astype(pd.Float64Dtype()) - bigframes.testing.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) @pytest.mark.parametrize( @@ -101,7 +103,7 @@ def test_df_binary_ufuncs(scalars_dfs, opname): bf_result = op(scalars_df[["float64_col", "int64_col"]], 5.1).to_pandas() pd_result = op(scalars_pandas_df[["float64_col", "int64_col"]], 5.1) - bigframes.testing.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) # Operations tested here don't work on full dataframe in numpy+pandas @@ -133,7 +135,9 @@ def test_series_binary_ufuncs(scalars_dfs, x, y, opname): bf_result = op(scalars_df[x], scalars_df[y]).to_pandas() pd_result = op(scalars_pandas_df[x], scalars_pandas_df[y]) - bigframes.testing.assert_series_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, nulls_are_nan=True + ) def test_series_binary_ufuncs_reverse(scalars_dfs): @@ -143,7 +147,9 @@ def test_series_binary_ufuncs_reverse(scalars_dfs): bf_result = np.subtract(5.1, scalars_df["int64_col"]).to_pandas() pd_result = np.subtract(5.1, scalars_pandas_df["int64_col"]) - bigframes.testing.assert_series_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, nulls_are_nan=True + ) def test_df_binary_ufuncs_reverse(scalars_dfs): @@ -156,4 +162,4 @@ def test_df_binary_ufuncs_reverse(scalars_dfs): scalars_pandas_df[["float64_col", "int64_col"]], ) - bigframes.testing.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result, nulls_are_nan=True) diff --git a/tests/system/small/test_pandas.py b/tests/system/small/test_pandas.py index d83955ecde7..33c7364b5e7 100644 --- a/tests/system/small/test_pandas.py +++ b/tests/system/small/test_pandas.py @@ -65,7 +65,7 @@ def test_concat_series(scalars_dfs): ] ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -602,7 +602,7 @@ def test_cut_for_array(): bf_result = bpd.cut(sc, x) pd_result = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -621,7 +621,7 @@ def test_cut_by_int_bins(scalars_dfs, labels, right): bf_result = bpd.cut(scalars_df["float64_col"], 5, labels=labels, right=right) pd_result = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_cut_by_int_bins_w_labels(scalars_dfs): @@ -632,7 +632,7 @@ def test_cut_by_int_bins_w_labels(scalars_dfs): bf_result = bpd.cut(scalars_df["float64_col"], 5, labels=labels) pd_result = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -675,7 +675,7 @@ def test_cut_by_numeric_breaks(scalars_dfs, breaks, right, labels): ).to_pandas() pd_result_converted = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result, pd_result_converted) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result_converted) def test_cut_by_numeric_breaks_w_labels(scalars_dfs): @@ -687,7 +687,7 @@ def test_cut_by_numeric_breaks_w_labels(scalars_dfs): bf_result = bpd.cut(scalars_df["float64_col"], bins, labels=labels) pd_result = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -727,7 +727,7 @@ def test_cut_by_interval_bins(scalars_dfs, bins, right, labels): pd_result = pd.cut(scalars_pandas_df["int64_too"], bins, labels=labels, right=right) pd_result_converted = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result, pd_result_converted) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result_converted) def test_cut_by_interval_bins_w_labels(scalars_dfs): @@ -739,7 +739,7 @@ def test_cut_by_interval_bins_w_labels(scalars_dfs): bf_result = bpd.cut(scalars_df["float64_col"], bins, labels=labels) pd_result = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -756,7 +756,7 @@ def test_cut_by_edge_cases_bins(scalars_dfs, bins, labels): pd_result = pd.cut(scalars_pandas_df["int64_too"], bins, labels=labels) pd_result_converted = _convert_pandas_category(pd_result) - bigframes.testing.assert_series_equal(bf_result, pd_result_converted) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result_converted) def test_cut_empty_array_raises_error(): @@ -785,7 +785,7 @@ def test_qcut(scalars_dfs, q): bf_result = bpd.qcut(scalars_df["float64_col"], q, labels=False, duplicates="drop") pd_result = pd_result.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -832,7 +832,7 @@ def test_to_datetime_iterable(arg, utc, unit, format): .dt.floor("us") .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -846,7 +846,7 @@ def test_to_datetime_series(scalars_dfs): pd_result = pd.Series(pd.to_datetime(scalars_pandas_df[col], unit="s")).astype( "datetime64[s]" ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -872,7 +872,7 @@ def test_to_datetime_unit_param(arg, unit): .dt.floor("us") .astype("datetime64[ns]") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -897,7 +897,7 @@ def test_to_datetime_format_param(arg, utc, format): .dt.floor("us") .astype("datetime64[ns, UTC]" if utc else "datetime64[ns]") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -955,7 +955,7 @@ def test_to_datetime_string_inputs(arg, utc, output_in_utc, format): .astype(normalized_type) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -999,7 +999,7 @@ def test_to_datetime_timestamp_inputs(arg, utc, output_in_utc): pd.Series(pd.to_datetime(arg, utc=utc)).dt.floor("us").astype(normalized_type) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, check_names=False ) @@ -1064,7 +1064,7 @@ def test_to_timedelta_with_bf_float_series_value_rounded_down(session): expected_result = pd.Series([pd.Timedelta(1, "us"), pd.Timedelta(2, "us")]).astype( "timedelta64[ns]" ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -1085,7 +1085,7 @@ def test_to_timedelta_with_list_like_input(session, input): ) expected_result = pd.Series(pd.to_timedelta(input, "s")).astype("timedelta64[ns]") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) @@ -1116,6 +1116,6 @@ def test_to_timedelta_on_timedelta_series__should_be_no_op(scalars_dfs): ) expected_result = pd.to_timedelta(pd_series, unit="s").astype("timedelta64[ns]") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_index_type=False ) diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index 51d0cc61f04..90d3b9f8190 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -48,7 +48,7 @@ def test_series_construct_copy(scalars_dfs): pd_result = pd.Series( scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_nullable_ints(): @@ -63,7 +63,7 @@ def test_series_construct_nullable_ints(): ) expected = pd.Series([1, 3, pd.NA], dtype=pd.Int64Dtype(), index=expected_index) - bigframes.testing.assert_series_equal(bf_result, expected) + bigframes.testing.utils.assert_series_equal(bf_result, expected) def test_series_construct_timestamps(): @@ -75,7 +75,9 @@ def test_series_construct_timestamps(): bf_result = series.Series(datetimes).to_pandas() pd_result = pd.Series(datetimes, dtype=pd.ArrowDtype(pa.timestamp("us"))) - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_series_construct_copy_with_index(scalars_dfs): @@ -92,7 +94,7 @@ def test_series_construct_copy_with_index(scalars_dfs): dtype="Float64", index=scalars_pandas_df["int64_too"], ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_copy_index(scalars_dfs): @@ -109,7 +111,7 @@ def test_series_construct_copy_index(scalars_dfs): dtype="Float64", index=scalars_pandas_df["int64_too"], ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_pandas(scalars_dfs): @@ -121,7 +123,7 @@ def test_series_construct_pandas(scalars_dfs): scalars_pandas_df["int64_col"], name="test_series", dtype="Float64" ) assert bf_result.shape == pd_result.shape - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_series_construct_from_list(): @@ -131,7 +133,7 @@ def test_series_construct_from_list(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_reindex(): @@ -142,7 +144,7 @@ def test_series_construct_reindex(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_from_list_w_index(): @@ -156,7 +158,7 @@ def test_series_construct_from_list_w_index(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_empty(session: bigframes.Session): @@ -177,7 +179,7 @@ def test_series_construct_scalar_no_index(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_scalar_w_index(): @@ -189,7 +191,7 @@ def test_series_construct_scalar_w_index(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_nan(): @@ -199,7 +201,7 @@ def test_series_construct_nan(): pd_result.index = pd_result.index.astype("Int64") pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_scalar_w_bf_index(): @@ -210,7 +212,7 @@ def test_series_construct_scalar_w_bf_index(): pd_result = pd_result.astype("string[pyarrow]") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_construct_from_list_escaped_strings(): @@ -226,7 +228,7 @@ def test_series_construct_from_list_escaped_strings(): # BigQuery DataFrame default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_series_construct_geodata(): @@ -241,7 +243,7 @@ def test_series_construct_geodata(): series = bigframes.pandas.Series(pd_series) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series, series.to_pandas(), check_index_type=False ) @@ -259,7 +261,7 @@ def test_series_construct_w_dtype(dtype): expected = pd.Series(data, dtype=dtype) expected.index = expected.index.astype("Int64") series = bigframes.pandas.Series(data, dtype=dtype) - bigframes.testing.assert_series_equal(series.to_pandas(), expected) + bigframes.testing.utils.assert_series_equal(series.to_pandas(), expected) def test_series_construct_w_dtype_for_struct(): @@ -276,7 +278,7 @@ def test_series_construct_w_dtype_for_struct(): series = bigframes.pandas.Series(data, dtype=dtype) expected = pd.Series(data, dtype=dtype) expected.index = expected.index.astype("Int64") - bigframes.testing.assert_series_equal(series.to_pandas(), expected) + bigframes.testing.utils.assert_series_equal(series.to_pandas(), expected) def test_series_construct_w_dtype_for_array_string(): @@ -294,7 +296,7 @@ def test_series_construct_w_dtype_for_array_string(): else: check_dtype = False - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( series.to_pandas(), expected, check_dtype=check_dtype ) @@ -314,7 +316,7 @@ def test_series_construct_w_dtype_for_array_struct(): else: check_dtype = False - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( series.to_pandas(), expected, check_dtype=check_dtype ) @@ -324,7 +326,7 @@ def test_series_construct_local_unordered_has_sequential_index(unordered_session ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"], session=unordered_session ) expected: pd.Index = pd.Index([0, 1, 2, 3, 4, 5, 6], dtype=pd.Int64Dtype()) - bigframes.testing.assert_index_equal(series.index.to_pandas(), expected) + bigframes.testing.utils.assert_index_equal(series.index.to_pandas(), expected) @pytest.mark.parametrize( @@ -386,14 +388,14 @@ def test_series_construct_w_nested_json_dtype(): ), ) - bigframes.testing.assert_series_equal(s.to_pandas(), s2.to_pandas()) + bigframes.testing.utils.assert_series_equal(s.to_pandas(), s2.to_pandas()) def test_series_keys(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df["int64_col"].keys().to_pandas() pd_result = scalars_pandas_df["int64_col"].keys() - bigframes.testing.assert_index_equal(bf_result, pd_result) + bigframes.testing.utils.assert_index_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -538,7 +540,7 @@ def test_series___getitem__(scalars_dfs, index_col, key): scalars_pandas_df = scalars_pandas_df.set_index(index_col, drop=False) bf_result = scalars_df[col_name][key] pd_result = scalars_pandas_df[col_name][key] - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -592,7 +594,7 @@ def test_series___setitem__(scalars_dfs, index_col, key, value): bf_series[key] = value pd_series[key] = value - bigframes.testing.assert_series_equal(bf_series.to_pandas(), pd_series) + bigframes.testing.utils.assert_series_equal(bf_series.to_pandas(), pd_series) @pytest.mark.parametrize( @@ -617,7 +619,7 @@ def test_series___setitem___with_int_key_numeric(scalars_dfs, key, value): bf_series[key] = value pd_series[key] = value - bigframes.testing.assert_series_equal(bf_series.to_pandas(), pd_series) + bigframes.testing.utils.assert_series_equal(bf_series.to_pandas(), pd_series) def test_series___setitem___with_default_index(scalars_dfs): @@ -714,7 +716,7 @@ def test_series_replace_scalar_scalar(scalars_dfs): ) pd_result = scalars_pandas_df[col_name].replace("Hello, World!", "Howdy, Planet!") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -730,7 +732,7 @@ def test_series_replace_regex_scalar(scalars_dfs): "^H.l", "Howdy, Planet!", regex=True ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -748,7 +750,7 @@ def test_series_replace_list_scalar(scalars_dfs): ["Hello, World!", "T"], "Howdy, Planet!" ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -760,7 +762,7 @@ def test_series_replace_nans_with_pd_na(scalars_dfs): bf_result = scalars_df[col_name].replace({pd.NA: "UNKNOWN"}).to_pandas() pd_result = scalars_pandas_df[col_name].replace({pd.NA: "UNKNOWN"}) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -785,7 +787,7 @@ def test_series_replace_dict(scalars_dfs, replacement_dict): bf_result = scalars_df[col_name].replace(replacement_dict).to_pandas() pd_result = scalars_pandas_df[col_name].replace(replacement_dict) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -841,7 +843,9 @@ def test_series_dropna(scalars_dfs, ignore_index): col_name = "string_col" bf_result = scalars_df[col_name].dropna(ignore_index=ignore_index).to_pandas() pd_result = scalars_pandas_df[col_name].dropna(ignore_index=ignore_index) - bigframes.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -877,7 +881,9 @@ def test_series_agg_multi_string(scalars_dfs): # Pandas may produce narrower numeric types, but bigframes always produces Float64 pd_result = pd_result.astype("Float64") - bigframes.testing.assert_series_equal(pd_result, bf_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + pd_result, bf_result, check_index_type=False + ) @pytest.mark.parametrize( @@ -994,7 +1000,7 @@ def test_mode_stat(scalars_df_index, scalars_pandas_df_index, col_name): ## Mode implicitly resets index, and bigframes default indices use nullable Int64 pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -1162,7 +1168,7 @@ def test_mods(scalars_dfs, col_x, col_y, method): else: bf_result = bf_series.astype("Float64").to_pandas() pd_result = getattr(scalars_pandas_df[col_x], method)(scalars_pandas_df[col_y]) - bigframes.testing.assert_series_equal(pd_result, bf_result) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result) # We work around a pandas bug that doesn't handle correlating nullable dtypes by doing this @@ -1227,16 +1233,20 @@ def test_divmods_series(scalars_dfs, col_x, col_y, method): ) # BigQuery's mod functions return NUMERIC values for non-INT64 inputs. if bf_div_result.dtype == pd.Int64Dtype(): - bigframes.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + bigframes.testing.utils.assert_series_equal( + pd_div_result, bf_div_result.to_pandas() + ) else: - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_div_result, bf_div_result.astype("Float64").to_pandas() ) if bf_mod_result.dtype == pd.Int64Dtype(): - bigframes.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + bigframes.testing.utils.assert_series_equal( + pd_mod_result, bf_mod_result.to_pandas() + ) else: - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_mod_result, bf_mod_result.astype("Float64").to_pandas() ) @@ -1268,16 +1278,20 @@ def test_divmods_scalars(scalars_dfs, col_x, other, method): pd_div_result, pd_mod_result = getattr(scalars_pandas_df[col_x], method)(other) # BigQuery's mod functions return NUMERIC values for non-INT64 inputs. if bf_div_result.dtype == pd.Int64Dtype(): - bigframes.testing.assert_series_equal(pd_div_result, bf_div_result.to_pandas()) + bigframes.testing.utils.assert_series_equal( + pd_div_result, bf_div_result.to_pandas() + ) else: - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_div_result, bf_div_result.astype("Float64").to_pandas() ) if bf_mod_result.dtype == pd.Int64Dtype(): - bigframes.testing.assert_series_equal(pd_mod_result, bf_mod_result.to_pandas()) + bigframes.testing.utils.assert_series_equal( + pd_mod_result, bf_mod_result.to_pandas() + ) else: - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_mod_result, bf_mod_result.astype("Float64").to_pandas() ) @@ -1350,7 +1364,7 @@ def test_series_add_different_table_default_index( + scalars_df_2_default_index["float64_col"].to_pandas() ) # TODO(swast): Can remove sort_index() when there's default ordering. - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.sort_index(), pd_result.sort_index() ) @@ -1363,7 +1377,7 @@ def test_series_add_different_table_with_index( # When index values are unique, we can emulate with values from the same # DataFrame. pd_result = scalars_pandas_df["float64_col"] + scalars_pandas_df["int64_col"] - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_reset_index_drop(scalars_df_index, scalars_pandas_df_index): @@ -1382,7 +1396,7 @@ def test_reset_index_drop(scalars_df_index, scalars_pandas_df_index): # BigQuery DataFrames default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_series_reset_index_allow_duplicates(scalars_df_index, scalars_pandas_df_index): @@ -1401,7 +1415,7 @@ def test_series_reset_index_allow_duplicates(scalars_df_index, scalars_pandas_df pd_result.index = pd_result.index.astype(pd.Int64Dtype()) # reset_index should maintain the original ordering. - bigframes.testing.assert_frame_equal(bf_result, pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result, pd_result) def test_series_reset_index_duplicates_error(scalars_df_index): @@ -1420,7 +1434,7 @@ def test_series_reset_index_inplace(scalars_df_index, scalars_pandas_df_index): # BigQuery DataFrames default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) @pytest.mark.parametrize( @@ -1447,7 +1461,7 @@ def test_reset_index_no_drop(scalars_df_index, scalars_pandas_df_index, name): # BigQuery DataFrames default indices use nullable Int64 always pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_frame_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_frame_equal(bf_result.to_pandas(), pd_result) def test_copy(scalars_df_index, scalars_pandas_df_index): @@ -1464,7 +1478,7 @@ def test_copy(scalars_df_index, scalars_pandas_df_index): pd_series.loc[0] = 3.4 assert bf_copy.to_pandas().loc[0] != bf_series.to_pandas().loc[0] - bigframes.testing.assert_series_equal(bf_copy.to_pandas(), pd_copy) + bigframes.testing.utils.assert_series_equal(bf_copy.to_pandas(), pd_copy) def test_isin_raise_error(scalars_df_index, scalars_pandas_df_index): @@ -1505,7 +1519,7 @@ def test_isin(scalars_dfs, col_name, test_set): scalars_df, scalars_pandas_df = scalars_dfs bf_result = scalars_df[col_name].isin(test_set).to_pandas() pd_result = scalars_pandas_df[col_name].isin(test_set).astype("boolean") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -1545,7 +1559,7 @@ def test_isin_bigframes_values(scalars_dfs, col_name, test_set, session): scalars_df[col_name].isin(series.Series(test_set, session=session)).to_pandas() ) pd_result = scalars_pandas_df[col_name].isin(test_set).astype("boolean") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -1563,7 +1577,7 @@ def test_isin_bigframes_index(scalars_dfs, session): .isin(pd.Index(["Hello, World!", "Hi", "こんにちは"])) .astype("boolean") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result, ) @@ -1608,7 +1622,7 @@ def test_isin_bigframes_values_as_predicate( pd_predicate = scalars_pandas_df[col_name].isin(test_set) pd_result = scalars_pandas_df[pd_predicate] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( pd_result.reset_index(), bf_result.reset_index(), ) @@ -1709,10 +1723,10 @@ def test_loc_setitem_cell(scalars_df_index, scalars_pandas_df_index): pd_series.loc[2] = "This value isn't in the test data." bf_result = bf_series.to_pandas() pd_result = pd_series - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) # Per Copy-on-Write semantics, other references to the original DataFrame # should remain unchanged. - bigframes.testing.assert_series_equal(bf_original.to_pandas(), pd_original) + bigframes.testing.utils.assert_series_equal(bf_original.to_pandas(), pd_original) def test_at_setitem_row_label_scalar(scalars_dfs): @@ -1723,7 +1737,7 @@ def test_at_setitem_row_label_scalar(scalars_dfs): pd_series.at[1] = 1000 bf_result = bf_series.to_pandas() pd_result = pd_series.astype("Int64") - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_ne_obj_series(scalars_dfs): @@ -2007,7 +2021,7 @@ def test_series_quantile(scalars_dfs): pd_result = pd_series.quantile([0.0, 0.4, 0.6, 1.0]) bf_result = bf_series.quantile([0.0, 0.4, 0.6, 1.0]) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result.to_pandas(), check_dtype=False, check_index_type=False ) @@ -2056,7 +2070,7 @@ def test_cumprod(scalars_dfs): col_name = "float64_col" bf_result = scalars_df[col_name].cumprod() pd_result = scalars_pandas_df[col_name].cumprod() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_result, bf_result.to_pandas(), ) @@ -2157,7 +2171,7 @@ def test_groupby_level_sum(scalars_dfs): bf_series = scalars_df[col_name].groupby(level=0).sum() pd_series = scalars_pandas_df[col_name].groupby(level=0).sum() # TODO(swast): Update groupby to use index based on group by key(s). - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series.sort_index(), bf_series.to_pandas().sort_index(), ) @@ -2171,7 +2185,7 @@ def test_groupby_level_list_sum(scalars_dfs): bf_series = scalars_df[col_name].groupby(level=["rowindex"]).sum() pd_series = scalars_pandas_df[col_name].groupby(level=["rowindex"]).sum() # TODO(swast): Update groupby to use index based on group by key(s). - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series.sort_index(), bf_series.to_pandas().sort_index(), ) @@ -2288,7 +2302,7 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) scalars_pandas_df_index[col_name].groupby(scalars_pandas_df_index[group_key]) ).astype(bf_series.dtype) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series, bf_series, ) @@ -2304,7 +2318,7 @@ def test_groupby_window_ops(scalars_df_index, scalars_pandas_df_index, operator) def test_drop_label(scalars_df_index, scalars_pandas_df_index, label, col_name): bf_series = scalars_df_index[col_name].drop(label).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop(label) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series, bf_series, ) @@ -2314,7 +2328,7 @@ def test_drop_label_list(scalars_df_index, scalars_pandas_df_index): col_name = "int64_col" bf_series = scalars_df_index[col_name].drop([1, 3]).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop([1, 3]) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series, bf_series, ) @@ -2338,7 +2352,7 @@ def test_drop_label_list(scalars_df_index, scalars_pandas_df_index): def test_drop_duplicates(scalars_df_index, scalars_pandas_df_index, keep, col_name): bf_series = scalars_df_index[col_name].drop_duplicates(keep=keep).to_pandas() pd_series = scalars_pandas_df_index[col_name].drop_duplicates(keep=keep) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd_series, bf_series, ) @@ -2375,7 +2389,7 @@ def test_unique(scalars_df_index, scalars_pandas_df_index, col_name): def test_duplicated(scalars_df_index, scalars_pandas_df_index, keep, col_name): bf_series = scalars_df_index[col_name].duplicated(keep=keep).to_pandas() pd_series = scalars_pandas_df_index[col_name].duplicated(keep=keep) - bigframes.testing.assert_series_equal(pd_series, bf_series, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_series, bf_series, check_dtype=False) def test_shape(scalars_dfs): @@ -2509,7 +2523,7 @@ def test_head_then_scalar_operation(scalars_dfs): bf_result = (scalars_df["float64_col"].head(1) + 4).to_pandas() pd_result = scalars_pandas_df["float64_col"].head(1) + 4 - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2525,7 +2539,7 @@ def test_head_then_series_operation(scalars_dfs): "float64_col" ].head(2) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2536,7 +2550,7 @@ def test_series_peek(scalars_dfs): peek_result = scalars_df["float64_col"].peek(n=3, force=False) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, scalars_pandas_df["float64_col"].reindex_like(peek_result), ) @@ -2555,7 +2569,7 @@ def test_series_peek_with_large_results_not_allowed(scalars_dfs): # The metrics won't be fully updated when we call query_and_wait. print(session.slot_millis_sum - slot_millis_sum) assert session.slot_millis_sum - slot_millis_sum < 500 - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, scalars_pandas_df["float64_col"].reindex_like(peek_result), ) @@ -2569,7 +2583,7 @@ def test_series_peek_multi_index(scalars_dfs): pd_series = scalars_pandas_df.set_index(["string_col", "bool_col"])["float64_col"] pd_series.name = ("2-part", "name") peek_result = bf_series.peek(n=3, force=False) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, pd_series.reindex_like(peek_result), ) @@ -2581,7 +2595,7 @@ def test_series_peek_filtered(scalars_dfs): n=3, force=False ) pd_result = scalars_pandas_df[scalars_pandas_df.int64_col > 0]["float64_col"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, pd_result.reindex_like(peek_result), ) @@ -2597,7 +2611,7 @@ def test_series_peek_force(scalars_dfs): peek_result = df_filtered.peek(n=3, force=True) pd_cumsum_df = scalars_pandas_df[["int64_col", "int64_too"]].cumsum() pd_result = pd_cumsum_df[pd_cumsum_df.int64_col > 0]["int64_too"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, pd_result.reindex_like(peek_result), ) @@ -2613,7 +2627,7 @@ def test_series_peek_force_float(scalars_dfs): peek_result = df_filtered.peek(n=3, force=True) pd_cumsum_df = scalars_pandas_df[["int64_col", "float64_col"]].cumsum() pd_result = pd_cumsum_df[pd_cumsum_df.float64_col > 0]["float64_col"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( peek_result, pd_result.reindex_like(peek_result), ) @@ -2625,7 +2639,7 @@ def test_shift(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].shift().astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2636,7 +2650,7 @@ def test_series_ffill(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_name].ffill(limit=1).to_pandas() pd_result = scalars_pandas_df_index[col_name].ffill(limit=1) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2647,7 +2661,7 @@ def test_series_bfill(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_name].bfill(limit=2).to_pandas() pd_result = scalars_pandas_df_index[col_name].bfill(limit=2) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2662,7 +2676,7 @@ def test_cumsum_int(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2683,7 +2697,7 @@ def test_cumsum_int_ordered(scalars_df_index, scalars_pandas_df_index): .astype(pd.Int64Dtype()) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2702,7 +2716,7 @@ def test_series_nlargest(scalars_df_index, scalars_pandas_df_index, keep): bf_result = scalars_df_index[col_name].nlargest(4, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nlargest(4, keep=keep) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2725,7 +2739,7 @@ def test_diff(scalars_df_index, scalars_pandas_df_index, periods): .astype(pd.Int64Dtype()) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2744,7 +2758,7 @@ def test_series_pct_change(scalars_df_index, scalars_pandas_df_index, periods): # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index["int64_col"].ffill().pct_change(periods=periods) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2763,7 +2777,7 @@ def test_series_nsmallest(scalars_df_index, scalars_pandas_df_index, keep): bf_result = scalars_df_index[col_name].nsmallest(2, keep=keep).to_pandas() pd_result = scalars_pandas_df_index[col_name].nsmallest(2, keep=keep) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2812,7 +2826,7 @@ def test_series_rank( .astype(pd.Float64Dtype()) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2824,7 +2838,7 @@ def test_cast_float_to_int(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2836,7 +2850,7 @@ def test_cast_float_to_bool(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].astype(pd.BooleanDtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2854,7 +2868,7 @@ def test_cumsum_nested(scalars_df_index, scalars_pandas_df_index): .astype(pd.Float64Dtype()) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2883,7 +2897,7 @@ def test_nested_analytic_ops_align(scalars_df_index, scalars_pandas_df_index): + pd_series.expanding().max() ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2899,7 +2913,7 @@ def test_cumsum_int_filtered(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable ints in pandas, produces object type and never ignores NA pd_result = pd_col[pd_col > -2].cumsum().astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2911,7 +2925,7 @@ def test_cumsum_float(scalars_df_index, scalars_pandas_df_index): # cumsum does not behave well on nullable floats in pandas, produces object type and never ignores NA pd_result = scalars_pandas_df_index[col_name].cumsum().astype(pd.Float64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2922,7 +2936,7 @@ def test_cummin_int(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_name].cummin().to_pandas() pd_result = scalars_pandas_df_index[col_name].cummin() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2933,7 +2947,7 @@ def test_cummax_int(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index[col_name].cummax().to_pandas() pd_result = scalars_pandas_df_index[col_name].cummax() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -2966,7 +2980,7 @@ def test_value_counts(scalars_dfs, kwargs): bf_result = s.value_counts(**kwargs).to_pandas() pd_result = pd_s.value_counts(**kwargs) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3004,7 +3018,7 @@ def test_value_counts_w_cut(scalars_dfs): pd_result = pd_cut.value_counts() pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result.astype(pd.Int64Dtype()), ) @@ -3014,7 +3028,7 @@ def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index["string_col"].iloc[1:].iloc[1:].to_pandas() pd_result = scalars_pandas_df_index["string_col"].iloc[1:].iloc[1:] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3043,7 +3057,7 @@ def test_iloc_nested(scalars_df_index, scalars_pandas_df_index): def test_series_iloc(scalars_df_index, scalars_pandas_df_index, start, stop, step): bf_result = scalars_df_index["string_col"].iloc[start:stop:step].to_pandas() pd_result = scalars_pandas_df_index["string_col"].iloc[start:stop:step] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3079,7 +3093,7 @@ def test_series_add_prefix(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index["int64_too"].add_prefix("prefix_") # Index will be object type in pandas, string type in bigframes, but same values - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, @@ -3092,7 +3106,7 @@ def test_series_add_suffix(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index["int64_too"].add_suffix("_suffix") # Index will be object type in pandas, string type in bigframes, but same values - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_index_type=False, @@ -3120,7 +3134,7 @@ def test_series_filter_like(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index["float64_col"].filter(like="ello") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3134,7 +3148,7 @@ def test_series_filter_regex(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index["float64_col"].filter(regex="^[GH].*") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3149,7 +3163,7 @@ def test_series_reindex(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3176,7 +3190,7 @@ def test_series_reindex_like(scalars_df_index, scalars_pandas_df_index): # Pandas uses int64 instead of Int64 (nullable) dtype. pd_result.index = pd_result.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3192,7 +3206,7 @@ def test_where_with_series(scalars_df_index, scalars_pandas_df_index): scalars_pandas_df_index["bool_col"], scalars_pandas_df_index["int64_too"] ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3217,7 +3231,7 @@ def test_where_with_different_indices(scalars_df_index, scalars_pandas_df_index) ) ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3231,7 +3245,7 @@ def test_where_with_default(scalars_df_index, scalars_pandas_df_index): scalars_pandas_df_index["bool_col"] ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3251,7 +3265,7 @@ def _is_positive(x): cond=_is_positive, other=lambda x: x * 10 ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3300,7 +3314,7 @@ def test_clip_filtered_two_sided(scalars_df_index, scalars_pandas_df_index): upper_pd = scalars_pandas_df_index["int64_too"].iloc[:5] + 1 pd_result = col_pd.clip(lower_pd, upper_pd) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3315,7 +3329,7 @@ def test_clip_filtered_one_sided(scalars_df_index, scalars_pandas_df_index): lower_pd = scalars_pandas_df_index["int64_too"].iloc[2:] - 1 pd_result = col_pd.clip(lower_pd, None) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3345,7 +3359,7 @@ def test_between(scalars_df_index, scalars_pandas_df_index, left, right, inclusi ) pd_result = scalars_pandas_df_index["int64_col"].between(left, right, inclusive) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result.astype(pd.BooleanDtype()), ) @@ -3383,7 +3397,7 @@ def test_series_case_when(scalars_dfs_maybe_ordered): bf_result = bf_series.case_when(bf_conditions).to_pandas() pd_result = pd_series.case_when(pd_conditions) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result.astype(pd.Int64Dtype()), ) @@ -3419,7 +3433,7 @@ def test_series_case_when_change_type(scalars_dfs_maybe_ordered): bf_result = bf_series.case_when(bf_conditions).to_pandas() pd_result = pd_series.case_when(pd_conditions) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result.astype("string[pyarrow]"), ) @@ -3448,7 +3462,7 @@ def test_to_json(gcs_folder, scalars_df_index, scalars_pandas_df_index): scalars_df_index["int64_col"].to_json(path, lines=True, orient="records") gcs_df = pd.read_json(get_first_file_from_wildcard(path), lines=True) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( gcs_df["int64_col"].astype(pd.Int64Dtype()), scalars_pandas_df_index["int64_col"], check_dtype=False, @@ -3461,7 +3475,7 @@ def test_to_csv(gcs_folder, scalars_df_index, scalars_pandas_df_index): scalars_df_index["int64_col"].to_csv(path) gcs_df = pd.read_csv(get_first_file_from_wildcard(path)) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( gcs_df["int64_col"].astype(pd.Int64Dtype()), scalars_pandas_df_index["int64_col"], check_dtype=False, @@ -3590,7 +3604,7 @@ def test_series_values(scalars_df_index, scalars_pandas_df_index): pd_result = scalars_pandas_df_index["int64_too"].values # Numpy isn't equipped to compare non-numeric objects, so convert back to dataframe - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( pd.Series(bf_result), pd.Series(pd_result), check_dtype=False ) @@ -3623,7 +3637,7 @@ def test_sort_values(scalars_df_index, scalars_pandas_df_index, ascending, na_po ascending=ascending, na_position=na_position ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3636,7 +3650,7 @@ def test_series_sort_values_inplace(scalars_df_index, scalars_pandas_df_index): bf_result = bf_series.to_pandas() pd_result = scalars_pandas_df_index["int64_col"].sort_values(ascending=False) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3655,7 +3669,7 @@ def test_sort_index(scalars_df_index, scalars_pandas_df_index, ascending): ) pd_result = scalars_pandas_df_index["int64_too"].sort_index(ascending=ascending) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3667,7 +3681,7 @@ def test_series_sort_index_inplace(scalars_df_index, scalars_pandas_df_index): bf_result = bf_series.to_pandas() pd_result = scalars_pandas_df_index["int64_too"].sort_index(ascending=False) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3719,7 +3733,7 @@ def _ten_times(x): cond=lambda x: x > 0, other=_ten_times ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -3828,7 +3842,7 @@ def test_astype(scalars_df_index, scalars_pandas_df_index, column, to_type, erro pytest.importorskip("pandas", minversion="2.0.0") bf_result = scalars_df_index[column].astype(to_type, errors=errors).to_pandas() pd_result = scalars_pandas_df_index[column].astype(to_type) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_astype_python(session): @@ -3839,7 +3853,7 @@ def test_series_astype_python(session): index=pd.Index([0, 1, 2, 3], dtype="Int64"), ) result = session.read_pandas(input).astype(float, errors="null").to_pandas() - bigframes.testing.assert_series_equal(result, exepcted) + bigframes.testing.utils.assert_series_equal(result, exepcted) def test_astype_safe(session): @@ -3850,7 +3864,7 @@ def test_astype_safe(session): index=pd.Index([0, 1, 2, 3], dtype="Int64"), ) result = session.read_pandas(input).astype("Float64", errors="null").to_pandas() - bigframes.testing.assert_series_equal(result, exepcted) + bigframes.testing.utils.assert_series_equal(result, exepcted) def test_series_astype_w_invalid_error(session): @@ -3871,7 +3885,7 @@ def test_astype_numeric_to_int(scalars_df_index, scalars_pandas_df_index): .apply(lambda x: None if pd.isna(x) else math.trunc(x)) .astype(to_type) ) - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) @pytest.mark.parametrize( @@ -3889,7 +3903,7 @@ def test_date_time_astype_int( pytest.importorskip("pandas", minversion="2.0.0") bf_result = scalars_df_index[column].astype(to_type).to_pandas() pd_result = scalars_pandas_df_index[column].astype(to_type) - bigframes.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result, check_dtype=False) assert bf_result.dtype == "Int64" @@ -3900,7 +3914,9 @@ def test_string_astype_int(session): pd_result = pd_series.astype("Int64") bf_result = bf_series.astype("Int64").to_pandas() - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_string_astype_float(session): @@ -3913,7 +3929,9 @@ def test_string_astype_float(session): pd_result = pd_series.astype("Float64") bf_result = bf_series.astype("Float64").to_pandas() - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_string_astype_date(session): @@ -3933,7 +3951,9 @@ def test_string_astype_date(session): pd_result = pd_series.astype("date32[day][pyarrow]") # type: ignore bf_result = bf_series.astype("date32[day][pyarrow]").to_pandas() - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_string_astype_datetime(session): @@ -3946,7 +3966,9 @@ def test_string_astype_datetime(session): pd_result = pd_series.astype(pd.ArrowDtype(pa.timestamp("us"))) bf_result = bf_series.astype(pd.ArrowDtype(pa.timestamp("us"))).to_pandas() - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_string_astype_timestamp(session): @@ -3965,7 +3987,9 @@ def test_string_astype_timestamp(session): pd.ArrowDtype(pa.timestamp("us", tz="UTC")) ).to_pandas() - bigframes.testing.assert_series_equal(bf_result, pd_result, check_index_type=False) + bigframes.testing.utils.assert_series_equal( + bf_result, pd_result, check_index_type=False + ) def test_timestamp_astype_string(session): @@ -3987,7 +4011,7 @@ def test_timestamp_astype_string(session): ) bf_result = bf_series.astype(pa.string()).to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, expected_result, check_index_type=False, check_dtype=False ) assert bf_result.dtype == "string[pyarrow]" @@ -4003,7 +4027,7 @@ def test_float_astype_json(errors, session): expected_result = pd.Series(data, dtype=dtypes.JSON_DTYPE) expected_result.index = expected_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), expected_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), expected_result) def test_float_astype_json_str(session): @@ -4015,7 +4039,7 @@ def test_float_astype_json_str(session): expected_result = pd.Series(data, dtype=dtypes.JSON_DTYPE) expected_result.index = expected_result.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), expected_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), expected_result) @pytest.mark.parametrize("errors", ["raise", "null"]) @@ -4032,7 +4056,7 @@ def test_string_astype_json(errors, session): assert bf_result.dtype == dtypes.JSON_DTYPE pd_result = bf_series.to_pandas().astype(dtypes.JSON_DTYPE) - bigframes.testing.assert_series_equal(bf_result.to_pandas(), pd_result) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), pd_result) def test_string_astype_json_in_safe_mode(session): @@ -4043,7 +4067,7 @@ def test_string_astype_json_in_safe_mode(session): expected = pd.Series([None], dtype=dtypes.JSON_DTYPE) expected.index = expected.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), expected) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), expected) def test_string_astype_json_raise_error(session): @@ -4081,7 +4105,7 @@ def test_json_astype_others(data, to_type, errors, session): load_data = [json.loads(item) if item is not None else None for item in data] expected = pd.Series(load_data, dtype=to_type) expected.index = expected.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), expected) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), expected) @pytest.mark.parametrize( @@ -4115,7 +4139,7 @@ def test_json_astype_others_in_safe_mode(data, to_type, session): expected = pd.Series([None, None], dtype=to_type) expected.index = expected.index.astype("Int64") - bigframes.testing.assert_series_equal(bf_result.to_pandas(), expected) + bigframes.testing.utils.assert_series_equal(bf_result.to_pandas(), expected) @pytest.mark.parametrize( @@ -4138,7 +4162,7 @@ def test_loc_bool_series_explicit_index(scalars_df_index, scalars_pandas_df_inde bf_result = scalars_df_index.string_col.loc[scalars_df_index.bool_col].to_pandas() pd_result = scalars_pandas_df_index.string_col.loc[scalars_pandas_df_index.bool_col] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, ) @@ -4199,7 +4223,7 @@ def test_rename(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.rename("newname") pd_result = scalars_pandas_df_index.string_col.rename("newname") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4209,7 +4233,7 @@ def test_rename_nonstring(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.rename((4, 2)) pd_result = scalars_pandas_df_index.string_col.rename((4, 2)) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4221,7 +4245,7 @@ def test_rename_dict_same_type(scalars_df_index, scalars_pandas_df_index): pd_result.index = pd_result.index.astype("Int64") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4231,7 +4255,7 @@ def test_rename_axis(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.rename_axis("newindexname") pd_result = scalars_pandas_df_index.string_col.rename_axis("newindexname") - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4248,7 +4272,7 @@ def test_loc_list_string_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.loc[index_list] pd_result = scalars_pandas_df_index.string_col.loc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4260,7 +4284,7 @@ def test_loc_list_integer_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.bool_col.loc[index_list] pd_result = scalars_pandas_df_index.bool_col.loc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4276,7 +4300,7 @@ def test_loc_list_multiindex(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_multiindex.int64_too.loc[index_list] pd_result = scalars_pandas_df_multiindex.int64_too.loc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4288,7 +4312,7 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.string_col.iloc[index_list] pd_result = scalars_pandas_df_index.string_col.iloc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4302,7 +4326,7 @@ def test_iloc_list_nameless(scalars_df_index, scalars_pandas_df_index): pd_series = scalars_pandas_df_index.string_col.rename(None) pd_result = pd_series.iloc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4317,7 +4341,7 @@ def test_loc_list_nameless(scalars_df_index, scalars_pandas_df_index): pd_series = scalars_pandas_df_index.string_col.rename(None) pd_result = pd_series.loc[index_list] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4333,7 +4357,7 @@ def test_loc_bf_series_string_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.date_col.loc[bf_string_series] pd_result = scalars_pandas_df_index.date_col.loc[pd_string_series] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4351,7 +4375,7 @@ def test_loc_bf_series_multiindex(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_multiindex.int64_too.loc[bf_string_series] pd_result = scalars_pandas_df_multiindex.int64_too.loc[pd_string_series] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4364,7 +4388,7 @@ def test_loc_bf_index_integer_index(scalars_df_index, scalars_pandas_df_index): bf_result = scalars_df_index.date_col.loc[bf_index] pd_result = scalars_pandas_df_index.date_col.loc[pd_index] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4378,7 +4402,7 @@ def test_loc_single_index_with_duplicate(scalars_df_index, scalars_pandas_df_ind index = "Hello, World!" bf_result = scalars_df_index.date_col.loc[index] pd_result = scalars_pandas_df_index.date_col.loc[index] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4463,7 +4487,7 @@ def test_map_dict_input(scalars_dfs): pd_result = pd_result.astype("Int64") # pandas type differences bf_result = scalars_df.string_col.map(local_map) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4482,7 +4506,7 @@ def test_map_series_input(scalars_dfs): pd_result = scalars_pandas_df.int64_too.map(pd_map_series) bf_result = scalars_df.int64_too.map(bf_map_series) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result.to_pandas(), pd_result, ) @@ -4743,7 +4767,7 @@ def foo(x: int, y: int, df): def test_series_explode(data): s = bigframes.pandas.Series(data) pd_s = s.to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( s.explode().to_pandas(), pd_s.explode(), check_index_type=False, @@ -4789,7 +4813,7 @@ def test_series_explode_w_index(index, ignore_index): s = bigframes.pandas.Series(data, index=index) pd_s = pd.Series(data, index=index) # TODO(b/340885567): fix type error - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( s.explode(ignore_index=ignore_index).to_pandas(), # type: ignore pd_s.explode(ignore_index=ignore_index).astype(pd.Float64Dtype()), # type: ignore check_index_type=False, @@ -4814,7 +4838,7 @@ def test_series_explode_reserve_order(ignore_index, ordered): # TODO(b/340885567): fix type error pd_res = pd_s.explode(ignore_index=ignore_index).astype(pd.Int64Dtype()) # type: ignore pd_res.index = pd_res.index.astype(pd.Int64Dtype()) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( res if ordered else res.sort_index(), pd_res, ) @@ -4836,7 +4860,7 @@ def test_series_construct_empty_array(): dtype=pd.ArrowDtype(pa.list_(pa.float64())), index=pd.Index([0], dtype=pd.Int64Dtype()), ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( expected, s.to_pandas(), ) @@ -4853,7 +4877,7 @@ def test_series_construct_empty_array(): ) def test_series_explode_null(data): s = bigframes.pandas.Series(data) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( s.explode().to_pandas(), s.to_pandas().explode(), check_dtype=False, @@ -4880,7 +4904,7 @@ def test_resample(scalars_df_index, scalars_pandas_df_index, append, level, col, pd_result = scalars_pandas_df_index.resample(rule=rule, level=level).min() # TODO: (b/484364312) pd_result.index.names = bf_result.index.names - bigframes.testing.assert_series_equal(bf_result, pd_result) + bigframes.testing.utils.assert_series_equal(bf_result, pd_result) def test_series_struct_get_field_by_attribute( @@ -4892,13 +4916,13 @@ def test_series_struct_get_field_by_attribute( bf_series = nested_structs_df["person"] df_series = nested_structs_pandas_df["person"] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_series.address.city.to_pandas(), df_series.struct.field("address").struct.field("city"), check_dtype=False, check_index=False, ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_series.address.country.to_pandas(), df_series.struct.field("address").struct.field("country"), check_dtype=False, diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 2fa633a62ba..e8e601cc76a 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -327,7 +327,7 @@ def test_read_gbq_w_anonymous_query_results_table(session: bigframes.Session): df = session.read_gbq(destination, index_col="name") result = df.to_pandas() expected.index = expected.index.astype(result.index.dtype) - bigframes.testing.assert_frame_equal(result, expected, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(result, expected, check_dtype=False) def test_read_gbq_w_primary_keys_table( @@ -350,7 +350,7 @@ def test_read_gbq_w_primary_keys_table( # Verify that the DataFrame is already sorted by primary keys. sorted_result = result.sort_values(primary_keys) - bigframes.testing.assert_frame_equal(result, sorted_result) + bigframes.testing.utils.assert_frame_equal(result, sorted_result) # Verify that we're working from a snapshot rather than a copy of the table. assert "FOR SYSTEM_TIME AS OF" in df.sql @@ -389,7 +389,7 @@ def test_read_gbq_w_primary_keys_table_and_filters( # Verify that the DataFrame is already sorted by primary keys. sorted_result = result.sort_values(primary_keys) - bigframes.testing.assert_frame_equal(result, sorted_result) + bigframes.testing.utils.assert_frame_equal(result, sorted_result) @pytest.mark.parametrize( @@ -534,7 +534,7 @@ def test_read_gbq_w_ambigous_name( .to_pandas() ) pd_df = pd.DataFrame({"x": [2, 1], "ambiguous_name": [20, 10]}) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( df, pd_df, check_dtype=False, check_index_type=False ) @@ -771,8 +771,10 @@ def test_read_gbq_w_json_and_compare_w_pandas_json(session): dtype=pd.ArrowDtype(db_dtypes.JSONArrowType()), ) pd_df.index = pd_df.index.astype("Int64") - bigframes.testing.assert_series_equal(df.dtypes, pd_df.dtypes) - bigframes.testing.assert_series_equal(df["json_col"].to_pandas(), pd_df["json_col"]) + bigframes.testing.utils.assert_series_equal(df.dtypes, pd_df.dtypes) + bigframes.testing.utils.assert_series_equal( + df["json_col"].to_pandas(), pd_df["json_col"] + ) def test_read_gbq_w_json_in_struct(session): @@ -870,7 +872,7 @@ def test_read_pandas(session, scalars_dfs): result = df.to_pandas() expected = scalars_pandas_df - bigframes.testing.assert_frame_equal(result, expected) + bigframes.testing.utils.assert_frame_equal(result, expected) def test_read_pandas_series(session): @@ -878,14 +880,14 @@ def test_read_pandas_series(session): pd_series = pd.Series([3, 1, 4, 1, 5], dtype=pd.Int64Dtype(), index=idx) bf_series = session.read_pandas(pd_series) - bigframes.testing.assert_series_equal(bf_series.to_pandas(), pd_series) + bigframes.testing.utils.assert_series_equal(bf_series.to_pandas(), pd_series) def test_read_pandas_index(session): pd_idx: pd.Index = pd.Index([2, 7, 1, 2, 8], dtype=pd.Int64Dtype()) bf_idx = session.read_pandas(pd_idx) - bigframes.testing.assert_index_equal(bf_idx.to_pandas(), pd_idx) + bigframes.testing.utils.assert_index_equal(bf_idx.to_pandas(), pd_idx) def test_read_pandas_w_unsupported_mixed_dtype(session): @@ -915,7 +917,7 @@ def test_read_pandas_col_label_w_space(session: bigframes.Session): ) result = session.read_pandas(expected).to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( result, expected, check_index_type=False, check_dtype=False ) @@ -923,7 +925,7 @@ def test_read_pandas_col_label_w_space(session: bigframes.Session): def test_read_pandas_multi_index(session, scalars_pandas_df_multi_index): df = session.read_pandas(scalars_pandas_df_multi_index) result = df.to_pandas() - bigframes.testing.assert_frame_equal(result, scalars_pandas_df_multi_index) + bigframes.testing.utils.assert_frame_equal(result, scalars_pandas_df_multi_index) def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default_index): @@ -931,7 +933,9 @@ def test_read_pandas_rowid_exists_adds_suffix(session, scalars_pandas_df_default pandas_df["rowid"] = np.arange(pandas_df.shape[0]) df_roundtrip = session.read_pandas(pandas_df).to_pandas() - bigframes.testing.assert_frame_equal(df_roundtrip, pandas_df, check_dtype=False) + bigframes.testing.utils.assert_frame_equal( + df_roundtrip, pandas_df, check_dtype=False + ) def test_read_pandas_tokyo( @@ -970,7 +974,7 @@ def test_read_pandas_timedelta_dataframes(session, write_engine): expected_result = pandas_df.astype(bigframes.dtypes.TIMEDELTA_DTYPE) expected_result.index = expected_result.index.astype(bigframes.dtypes.INT_DTYPE) - bigframes.testing.assert_frame_equal(actual_result, expected_result) + bigframes.testing.utils.assert_frame_equal(actual_result, expected_result) @all_write_engines @@ -985,7 +989,7 @@ def test_read_pandas_timedelta_series(session, write_engine): .astype("timedelta64[ns]") ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_series, check_index_type=False ) @@ -1002,7 +1006,7 @@ def test_read_pandas_timedelta_index(session, write_engine): .astype("timedelta64[ns]") ) - bigframes.testing.assert_index_equal(actual_result, expected_index) + bigframes.testing.utils.assert_index_equal(actual_result, expected_index) @all_write_engines @@ -1021,7 +1025,7 @@ def test_read_pandas_json_dataframes(session, write_engine): expected_df, write_engine=write_engine ).to_pandas() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_df, check_index_type=False ) @@ -1039,7 +1043,7 @@ def test_read_pandas_json_series(session, write_engine): actual_result = session.read_pandas( expected_series, write_engine=write_engine ).to_pandas() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_series, check_index_type=False ) @@ -1067,7 +1071,7 @@ def test_read_pandas_json_index(session, write_engine): actual_result = session.read_pandas( expected_index, write_engine=write_engine ).to_pandas() - bigframes.testing.assert_index_equal(actual_result, expected_index) + bigframes.testing.utils.assert_index_equal(actual_result, expected_index) @pytest.mark.parametrize( @@ -1128,7 +1132,7 @@ def test_read_pandas_w_nested_json(session, write_engine): .to_pandas() .reset_index(drop=True) ) - bigframes.testing.assert_series_equal(bq_s, pd_s) + bigframes.testing.utils.assert_series_equal(bq_s, pd_s) @pytest.mark.parametrize( @@ -1212,7 +1216,7 @@ def test_read_pandas_w_nested_json_index(session, write_engine): ), ) bq_idx = session.read_pandas(pd_idx, write_engine=write_engine).to_pandas() - bigframes.testing.assert_index_equal(bq_idx, pd_idx) + bigframes.testing.utils.assert_index_equal(bq_idx, pd_idx) @all_write_engines @@ -1226,13 +1230,15 @@ def test_read_csv_for_gcs_file_w_write_engine(session, df_and_gcs_csv, write_eng write_engine=write_engine, dtype=scalars_df.dtypes.to_dict(), ) - bigframes.testing.assert_frame_equal(pd_df.to_pandas(), scalars_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal( + pd_df.to_pandas(), scalars_df.to_pandas() + ) if write_engine in ("default", "bigquery_load"): bf_df = session.read_csv( path, engine="bigquery", index_col="rowindex", write_engine=write_engine ) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) @pytest.mark.parametrize( @@ -1260,8 +1266,10 @@ def test_read_csv_for_local_file_w_sep(session, df_and_local_csv, sep): pd_df = session.read_csv( buffer, index_col="rowindex", sep=sep, dtype=scalars_df.dtypes.to_dict() ) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), scalars_df.to_pandas()) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal( + bf_df.to_pandas(), scalars_df.to_pandas() + ) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) @pytest.mark.parametrize( @@ -1293,7 +1301,7 @@ def test_read_csv_for_index_col_w_false(session, df_and_local_csv, index_col): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index("rowindex").sort_index() pd_df = pd_df.set_index("rowindex") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) @pytest.mark.parametrize( @@ -1316,7 +1324,7 @@ def test_read_csv_for_index_col(session, df_and_gcs_csv, index_col): ) assert bf_df.shape == pd_df.shape - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) @pytest.mark.parametrize( @@ -1369,7 +1377,7 @@ def test_read_csv_for_gcs_wildcard_path(session, df_and_gcs_csv): assert bf_df.shape == pd_df.shape assert bf_df.columns.tolist() == pd_df.columns.tolist() - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_names(session, df_and_gcs_csv_for_two_columns): @@ -1388,7 +1396,7 @@ def test_read_csv_for_names(session, df_and_gcs_csv_for_two_columns): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index(names[0]).sort_index() pd_df = pd_df.set_index(names[0]) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_names_more_than_columns_can_raise_error( @@ -1417,7 +1425,7 @@ def test_read_csv_for_names_less_than_columns(session, df_and_gcs_csv_for_two_co # Pandas's index name is None, while BigFrames's index name is "rowindex". pd_df.index.name = "rowindex" - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.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( @@ -1455,7 +1463,7 @@ def test_read_csv_for_names_and_index_col( assert bf_df.shape == pd_df.shape assert bf_df.columns.tolist() == pd_df.columns.tolist() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_df.to_pandas(), pd_df.to_pandas(), check_index_type=False ) @@ -1487,7 +1495,7 @@ def test_read_csv_for_names_and_usecols( # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index(names[0]).sort_index() pd_df = pd_df.set_index(names[0]) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_names_and_invalid_usecols( @@ -1534,7 +1542,7 @@ def test_read_csv_for_names_and_usecols_and_indexcol( assert bf_df.shape == pd_df.shape assert bf_df.columns.tolist() == pd_df.columns.tolist() - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_names_less_than_columns_and_same_usecols( @@ -1557,7 +1565,7 @@ def test_read_csv_for_names_less_than_columns_and_same_usecols( # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index(names[0]).sort_index() pd_df = pd_df.set_index(names[0]) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_names_less_than_columns_and_mismatched_usecols( @@ -1602,7 +1610,7 @@ def test_read_csv_for_dtype(session, df_and_gcs_csv_for_two_columns): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index("rowindex").sort_index() pd_df = pd_df.set_index("rowindex") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_for_dtype_w_names(session, df_and_gcs_csv_for_two_columns): @@ -1622,7 +1630,7 @@ def test_read_csv_for_dtype_w_names(session, df_and_gcs_csv_for_two_columns): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index("a").sort_index() pd_df = pd_df.set_index("a") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) @pytest.mark.parametrize( @@ -1689,8 +1697,10 @@ def test_read_csv_for_gcs_file_w_header(session, df_and_gcs_csv, header): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index("rowindex").sort_index() pd_df = pd_df.set_index("rowindex") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), scalars_df.to_pandas()) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal( + bf_df.to_pandas(), scalars_df.to_pandas() + ) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_w_usecols(session, df_and_local_csv): @@ -1718,7 +1728,7 @@ def test_read_csv_w_usecols(session, df_and_local_csv): # (b/280889935) or guarantee row ordering. bf_df = bf_df.set_index("rowindex").sort_index() pd_df = pd_df.set_index("rowindex") - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_w_usecols_and_indexcol(session, df_and_local_csv): @@ -1744,7 +1754,7 @@ def test_read_csv_w_usecols_and_indexcol(session, df_and_local_csv): assert bf_df.shape == pd_df.shape assert bf_df.columns.tolist() == pd_df.columns.tolist() - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_csv_w_indexcol_not_in_usecols(session, df_and_local_csv): @@ -1799,10 +1809,10 @@ def test_read_csv_local_w_encoding(session, penguins_pandas_df_default_index): bf_df = session.read_csv( path, engine="bigquery", index_col="rowindex", encoding="ISO-8859-1" ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_df.to_pandas(), penguins_pandas_df_default_index ) - bigframes.testing.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) + bigframes.testing.utils.assert_frame_equal(bf_df.to_pandas(), pd_df.to_pandas()) def test_read_pickle_local(session, penguins_pandas_df_default_index, tmp_path): @@ -1811,7 +1821,7 @@ def test_read_pickle_local(session, penguins_pandas_df_default_index, tmp_path): penguins_pandas_df_default_index.to_pickle(path) df = session.read_pickle(path) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( penguins_pandas_df_default_index, df.to_pandas() ) @@ -1822,7 +1832,7 @@ def test_read_pickle_buffer(session, penguins_pandas_df_default_index): buffer.seek(0) df = session.read_pickle(buffer) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( penguins_pandas_df_default_index, df.to_pandas() ) @@ -1843,7 +1853,7 @@ def test_read_pickle_gcs(session, penguins_pandas_df_default_index, gcs_folder): penguins_pandas_df_default_index.to_pickle(path) df = session.read_pickle(path) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( penguins_pandas_df_default_index, df.to_pandas() ) @@ -1918,7 +1928,7 @@ def test_read_parquet_gcs( assert df_out.size != 0 pd_df_in = df_in.to_pandas() pd_df_out = df_out.to_pandas() - bigframes.testing.assert_frame_equal(pd_df_in, pd_df_out) + bigframes.testing.utils.assert_frame_equal(pd_df_in, pd_df_out) @pytest.mark.parametrize( @@ -1968,7 +1978,7 @@ def test_read_parquet_gcs_compressed( assert df_out.size != 0 pd_df_in = df_in.to_pandas() pd_df_out = df_out.to_pandas() - bigframes.testing.assert_frame_equal(pd_df_in, pd_df_out) + bigframes.testing.utils.assert_frame_equal(pd_df_in, pd_df_out) @pytest.mark.parametrize( @@ -2013,7 +2023,7 @@ def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): df = session.read_json(read_path, lines=True, orient="records", engine="bigquery") # The auto detects of BigQuery load job does not preserve any ordering of columns for json. - bigframes.testing.assert_index_equal( + bigframes.testing.utils.assert_index_equal( df.columns.sort_values(), scalars_df.columns.sort_values() ) @@ -2038,7 +2048,7 @@ def test_read_json_gcs_bq_engine(session, scalars_dfs, gcs_folder): ] ) assert df.shape[0] == scalars_df.shape[0] - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( df.dtypes.sort_index(), scalars_df.dtypes.sort_index() ) @@ -2064,7 +2074,7 @@ def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): orient="records", ) - bigframes.testing.assert_index_equal(df.columns, scalars_df.columns) + bigframes.testing.utils.assert_index_equal(df.columns, scalars_df.columns) # The auto detects of BigQuery load job have restrictions to detect the bytes, # numeric and geometry types, so they're skipped here. @@ -2078,7 +2088,7 @@ def test_read_json_gcs_default_engine(session, scalars_dfs, gcs_folder): scalars_df = scalars_df.drop(columns=["date_col", "datetime_col", "time_col"]) assert df.shape[0] == scalars_df.shape[0] - bigframes.testing.assert_series_equal(df.dtypes, scalars_df.dtypes) + bigframes.testing.utils.assert_series_equal(df.dtypes, scalars_df.dtypes) @pytest.mark.parametrize( @@ -2226,7 +2236,7 @@ def _assert_query_dry_run_stats_are_valid(result: pd.Series): ] ) - bigframes.testing.assert_index_equal(result.index, expected_index) + bigframes.testing.utils.assert_index_equal(result.index, expected_index) assert result["columnCount"] + result["indexLevel"] > 0 @@ -2246,5 +2256,5 @@ def _assert_table_dry_run_stats_are_valid(result: pd.Series): ] ) - bigframes.testing.assert_index_equal(result.index, expected_index) + bigframes.testing.utils.assert_index_equal(result.index, expected_index) assert result["columnCount"] == len(result["columnDtypes"]) diff --git a/tests/system/small/test_window.py b/tests/system/small/test_window.py index 843ac2a5812..61e1cac096d 100644 --- a/tests/system/small/test_window.py +++ b/tests/system/small/test_window.py @@ -19,7 +19,7 @@ import pytest from bigframes import dtypes -import bigframes.testing +import bigframes.testing.utils @pytest.fixture(scope="module") @@ -62,7 +62,7 @@ def test_dataframe_rolling_closed_param(rows_rolling_dfs, closed): actual_result = bf_df.rolling(window=3, closed=closed).sum().to_pandas() expected_result = pd_df.rolling(window=3, closed=closed).sum() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_result, check_dtype=False ) @@ -83,7 +83,7 @@ def test_dataframe_groupby_rolling_closed_param(rows_rolling_dfs, closed): expected_result = ( pd_df.groupby(pd_df["int64_too"] % 2).rolling(window=3, closed=closed).sum() ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result[check_columns], expected_result, check_dtype=False ) @@ -94,7 +94,7 @@ def test_dataframe_rolling_on(rows_rolling_dfs): actual_result = bf_df.rolling(window=3, on="int64_too").sum().to_pandas() expected_result = pd_df.rolling(window=3, on="int64_too").sum() - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_result, check_dtype=False ) @@ -121,7 +121,7 @@ def test_dataframe_groupby_rolling_on(rows_rolling_dfs): expected_result = ( pd_df.groupby(pd_df["int64_too"] % 2).rolling(window=3, on="float64_col").sum() ) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result[check_columns], expected_result, check_dtype=False ) @@ -140,7 +140,7 @@ def test_series_rolling_closed_param(rows_rolling_series, closed): actual_result = bf_series.rolling(window=3, closed=closed).sum().to_pandas() expected_result = df_series.rolling(window=3, closed=closed).sum() - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False ) @@ -159,7 +159,7 @@ def test_series_groupby_rolling_closed_param(rows_rolling_series, closed): expected_result = ( df_series.groupby(df_series % 2).rolling(window=3, closed=closed).sum() ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False ) @@ -195,7 +195,7 @@ def test_series_window_agg_ops(rows_rolling_series, windowing, agg_op): actual_result = agg_op(windowing(bf_series)).to_pandas() expected_result = agg_op(windowing(pd_series)) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( expected_result, actual_result, check_dtype=False ) @@ -236,7 +236,7 @@ def test_dataframe_window_agg_ops(scalars_dfs, windowing, agg_op): bf_result = agg_op(windowing(bf_df)).to_pandas() pd_result = agg_op(windowing(pd_df)) - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize( @@ -283,7 +283,7 @@ def test_dataframe_window_agg_func(scalars_dfs, windowing, func): pd_result = windowing(pd_df).agg(func) - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) def test_series_window_agg_single_func(scalars_dfs): @@ -296,7 +296,7 @@ def test_series_window_agg_single_func(scalars_dfs): pd_result = pd_series.expanding().agg("sum") - bigframes.testing.assert_series_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_series_equal(pd_result, bf_result, check_dtype=False) def test_series_window_agg_multi_func(scalars_dfs): @@ -309,7 +309,7 @@ def test_series_window_agg_multi_func(scalars_dfs): pd_result = pd_series.expanding().agg(["sum", np.mean]) - bigframes.testing.assert_frame_equal(pd_result, bf_result, check_dtype=False) + bigframes.testing.utils.assert_frame_equal(pd_result, bf_result, check_dtype=False) @pytest.mark.parametrize("closed", ["left", "right", "both", "neither"]) @@ -335,7 +335,7 @@ def test_series_range_rolling(range_rolling_dfs, window, closed, ascending): .rolling(window=window, closed=closed) .min() ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False, check_index=False ) @@ -356,7 +356,7 @@ def test_series_groupby_range_rolling(range_rolling_dfs): expected_result = ( pd_series.sort_index().groupby(pd_series % 2 == 0).rolling(window="3s").min() ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False, check_index=False ) @@ -387,7 +387,7 @@ def test_dataframe_range_rolling(range_rolling_dfs, window, closed, ascending): # Need to cast Pandas index type. Otherwise it uses DatetimeIndex that # does not exist in BigFrame expected_result.index = expected_result.index.astype(dtypes.TIMESTAMP_DTYPE) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_result, check_dtype=False, @@ -404,7 +404,7 @@ def test_dataframe_range_rolling_on(range_rolling_dfs): # Need to specify the column order because Pandas (seemingly) # re-arranges columns alphabetically cols = ["ts_col", "int_col", "float_col"] - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result[cols], expected_result[cols], check_dtype=False, @@ -428,7 +428,7 @@ def test_dataframe_groupby_range_rolling(range_rolling_dfs): pd_df.sort_values(on).groupby("int_col").rolling(window="3s", on=on).min() ) expected_result.index = expected_result.index.set_names("index", level=1) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( actual_result, expected_result, check_dtype=False, @@ -455,7 +455,7 @@ def test_range_rolling_order_info_lookup(range_rolling_dfs): .rolling(window="3s") .count() ) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( actual_result, expected_result, check_dtype=False, check_index=False ) diff --git a/tests/unit/bigquery/_operations/test_io.py b/tests/unit/bigquery/_operations/test_io.py index 97b38f86495..b5dc9544aa8 100644 --- a/tests/unit/bigquery/_operations/test_io.py +++ b/tests/unit/bigquery/_operations/test_io.py @@ -17,7 +17,6 @@ import pytest import bigframes.bigquery._operations.io -import bigframes.core.sql.io import bigframes.session @@ -36,6 +35,6 @@ def test_load_data(get_table_metadata_mock, mock_session): ) mock_session.read_gbq_query.assert_called_once() generated_sql = mock_session.read_gbq_query.call_args[0][0] - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" + expected = "LOAD DATA INTO `my-project.my_dataset.my_table` (\n `col1` INT64,\n `col2` STRING\n) FROM FILES (format='CSV', uris=['gs://bucket/path*'])" assert generated_sql == expected get_table_metadata_mock.assert_called_once() diff --git a/tests/unit/bigquery/test_ai.py b/tests/unit/bigquery/test_ai.py index c73e63b9db1..2cb876d39a5 100644 --- a/tests/unit/bigquery/test_ai.py +++ b/tests/unit/bigquery/test_ai.py @@ -91,7 +91,7 @@ def test_generate_embedding_with_dataframe(mock_dataframe, mock_session): expected_part_1 = "SELECT * FROM AI.GENERATE_EMBEDDING(" expected_part_2 = f"MODEL `{model_name}`," expected_part_3 = "(SELECT * FROM my_table)," - expected_part_4 = "STRUCT(256 AS OUTPUT_DIMENSIONALITY)" + expected_part_4 = "STRUCT(256 AS `OUTPUT_DIMENSIONALITY`)" assert expected_part_1 in query assert expected_part_2 in query @@ -117,7 +117,7 @@ def test_generate_embedding_with_series(mock_embedding_series, mock_session): assert f"MODEL `{model_name}`" in query assert "(SELECT my_col AS content FROM my_table)" in query assert ( - "STRUCT(0.0 AS START_SECOND, 10.0 AS END_SECOND, 5.0 AS INTERVAL_SECONDS)" + "STRUCT(0.0 AS `START_SECOND`, 10.0 AS `END_SECOND`, 5.0 AS `INTERVAL_SECONDS`)" in query ) @@ -180,7 +180,7 @@ def test_generate_text_with_dataframe(mock_dataframe, mock_session): expected_part_1 = "SELECT * FROM AI.GENERATE_TEXT(" expected_part_2 = f"MODEL `{model_name}`," expected_part_3 = "(SELECT * FROM my_table)," - expected_part_4 = "STRUCT(256 AS MAX_OUTPUT_TOKENS)" + expected_part_4 = "STRUCT(256 AS `MAX_OUTPUT_TOKENS`)" assert expected_part_1 in query assert expected_part_2 in query @@ -238,7 +238,7 @@ def test_generate_table_with_dataframe(mock_dataframe, mock_session): expected_part_1 = "SELECT * FROM AI.GENERATE_TABLE(" expected_part_2 = f"MODEL `{model_name}`," expected_part_3 = "(SELECT * FROM my_table)," - expected_part_4 = "STRUCT('col1 STRING, col2 INT64' AS output_schema)" + expected_part_4 = "STRUCT('col1 STRING, col2 INT64' AS `output_schema`)" assert expected_part_1 in query assert expected_part_2 in query @@ -264,7 +264,7 @@ def test_generate_table_with_options(mock_dataframe, mock_session): assert f"MODEL `{model_name}`" in query assert "(SELECT * FROM my_table)" in query assert ( - "STRUCT('col1 STRING' AS output_schema, 0.5 AS temperature, 100 AS max_output_tokens)" + "STRUCT('col1 STRING' AS `output_schema`, 0.5 AS `temperature`, 100 AS `max_output_tokens`)" in query ) @@ -287,7 +287,7 @@ def test_generate_table_with_mapping_schema(mock_dataframe, mock_session): expected_part_1 = "SELECT * FROM AI.GENERATE_TABLE(" expected_part_2 = f"MODEL `{model_name}`," expected_part_3 = "(SELECT * FROM my_table)," - expected_part_4 = "STRUCT('col1 STRING, col2 INT64' AS output_schema)" + expected_part_4 = "STRUCT('col1 STRING, col2 INT64' AS `output_schema`)" assert expected_part_1 in query assert expected_part_2 in query diff --git a/tests/unit/bigquery/test_mathematical.py b/tests/unit/bigquery/test_mathematical.py new file mode 100644 index 00000000000..a39aeb103c0 --- /dev/null +++ b/tests/unit/bigquery/test_mathematical.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. + +import bigframes.bigquery as bbq +import bigframes.core.col as col +import bigframes.core.expression as ex +import bigframes.dtypes as dtypes +import bigframes.operations as ops + + +def test_rand_returns_expression(): + expr = bbq.rand() + + assert isinstance(expr, col.Expression) + node = expr._value + assert isinstance(node, ex.OpExpression) + op = node.op + assert isinstance(op, ops.SqlScalarOp) + assert op.sql_template == "RAND()" + assert op._output_type == dtypes.FLOAT_DTYPE + assert not op.is_deterministic + assert len(node.inputs) == 0 diff --git a/tests/unit/bigquery/test_ml.py b/tests/unit/bigquery/test_ml.py index e5c957767b9..a68133225d4 100644 --- a/tests/unit/bigquery/test_ml.py +++ b/tests/unit/bigquery/test_ml.py @@ -167,14 +167,23 @@ def test_generate_text_with_pandas_dataframe(read_pandas_mock, read_gbq_query_mo assert "ML.GENERATE_TEXT" in generated_sql assert f"MODEL `{MODEL_NAME}`" in generated_sql assert "(SELECT * FROM `pandas_df`)" in generated_sql - assert "STRUCT(0.5 AS temperature" in generated_sql - assert "128 AS max_output_tokens" in generated_sql - assert "20 AS top_k" in generated_sql - assert "0.9 AS top_p" in generated_sql - assert "true AS flatten_json_output" in generated_sql - assert "['a', 'b'] AS stop_sequences" in generated_sql - assert "true AS ground_with_google_search" in generated_sql - assert "'TYPE' AS request_type" in generated_sql + assert "STRUCT(\n 0.5 AS `temperature`" in generated_sql + assert "128 AS `max_output_tokens`" in generated_sql + assert "20 AS `top_k`" in generated_sql + assert "0.9 AS `top_p`" in generated_sql + assert "TRUE AS `flatten_json_output`" in generated_sql + assert "['a', 'b'] AS `stop_sequences`" in generated_sql + assert "TRUE AS `ground_with_google_search`" in generated_sql + assert "'TYPE' AS `request_type`" in generated_sql + + +@mock.patch("bigframes.pandas.read_gbq_query") +def test_get_insights(read_gbq_query_mock): + ml_ops.get_insights(MODEL_SERIES) + read_gbq_query_mock.assert_called_once() + generated_sql = read_gbq_query_mock.call_args[0][0] + assert "ML.GET_INSIGHTS" in generated_sql + assert f"MODEL `{MODEL_NAME}`" in generated_sql @mock.patch("bigframes.pandas.read_gbq_query") @@ -201,6 +210,6 @@ def test_generate_embedding_with_pandas_dataframe( assert "ML.GENERATE_EMBEDDING" in generated_sql assert f"MODEL `{MODEL_NAME}`" in generated_sql assert "(SELECT * FROM `pandas_df`)" in generated_sql - assert "true AS flatten_json_output" in generated_sql - assert "'RETRIEVAL_DOCUMENT' AS task_type" in generated_sql - assert "256 AS output_dimensionality" in generated_sql + assert "STRUCT(\n TRUE AS `flatten_json_output`" in generated_sql + assert "'RETRIEVAL_DOCUMENT' AS `task_type`" in generated_sql + assert "256 AS `output_dimensionality`" in generated_sql diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/None/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/None/out.sql new file mode 100644 index 00000000000..6771527318f --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/None/out.sql @@ -0,0 +1,3 @@ +SELECT + AI.CLASSIFY(input => (`string_col`), categories => ['greeting', 'rejection']) AS `result` +FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/bigframes-dev.us.bigframes-default-connection/out.sql similarity index 100% rename from tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/out.sql rename to tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_classify/bigframes-dev.us.bigframes-default-connection/out.sql diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/None/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/None/out.sql new file mode 100644 index 00000000000..bae091982ea --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/None/out.sql @@ -0,0 +1,3 @@ +SELECT + AI.IF(prompt => (`string_col`, ' is the same as ', `string_col`)) AS `result` +FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/bigframes-dev.us.bigframes-default-connection/out.sql similarity index 100% rename from tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/out.sql rename to tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_if/bigframes-dev.us.bigframes-default-connection/out.sql diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/None/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/None/out.sql new file mode 100644 index 00000000000..6a16276734e --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/None/out.sql @@ -0,0 +1,3 @@ +SELECT + AI.SCORE(prompt => (`string_col`, ' is the same as ', `string_col`)) AS `result` +FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/bigframes-dev.us.bigframes-default-connection/out.sql similarity index 100% rename from tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/out.sql rename to tests/unit/core/compile/sqlglot/expressions/snapshots/test_ai_ops/test_ai_score/bigframes-dev.us.bigframes-default-connection/out.sql diff --git a/tests/unit/core/compile/sqlglot/expressions/test_ai_ops.py b/tests/unit/core/compile/sqlglot/expressions/test_ai_ops.py index c0cbece9054..64a5a94c9e7 100644 --- a/tests/unit/core/compile/sqlglot/expressions/test_ai_ops.py +++ b/tests/unit/core/compile/sqlglot/expressions/test_ai_ops.py @@ -281,12 +281,13 @@ def test_ai_generate_double_with_model_param( snapshot.assert_match(sql, "out.sql") -def test_ai_if(scalar_types_df: dataframe.DataFrame, snapshot): +@pytest.mark.parametrize("connection_id", [None, CONNECTION_ID]) +def test_ai_if(scalar_types_df: dataframe.DataFrame, snapshot, connection_id): col_name = "string_col" op = ops.AIIf( prompt_context=(None, " is the same as ", None), - connection_id=CONNECTION_ID, + connection_id=connection_id, ) sql = utils._apply_ops_to_sql( @@ -296,13 +297,14 @@ def test_ai_if(scalar_types_df: dataframe.DataFrame, snapshot): snapshot.assert_match(sql, "out.sql") -def test_ai_classify(scalar_types_df: dataframe.DataFrame, snapshot): +@pytest.mark.parametrize("connection_id", [None, CONNECTION_ID]) +def test_ai_classify(scalar_types_df: dataframe.DataFrame, snapshot, connection_id): col_name = "string_col" op = ops.AIClassify( prompt_context=(None,), categories=("greeting", "rejection"), - connection_id=CONNECTION_ID, + connection_id=connection_id, ) sql = utils._apply_ops_to_sql(scalar_types_df, [op.as_expr(col_name)], ["result"]) @@ -310,12 +312,13 @@ def test_ai_classify(scalar_types_df: dataframe.DataFrame, snapshot): snapshot.assert_match(sql, "out.sql") -def test_ai_score(scalar_types_df: dataframe.DataFrame, snapshot): +@pytest.mark.parametrize("connection_id", [None, CONNECTION_ID]) +def test_ai_score(scalar_types_df: dataframe.DataFrame, snapshot, connection_id): col_name = "string_col" op = ops.AIScore( prompt_context=(None, " is the same as ", None), - connection_id=CONNECTION_ID, + connection_id=connection_id, ) sql = utils._apply_ops_to_sql( diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat/out.sql index 3b0f9f0633d..48614357865 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat/out.sql @@ -1,41 +1,48 @@ WITH `bfcte_0` AS ( SELECT - `bfcol_9` AS `bfcol_30`, - `bfcol_10` AS `bfcol_31`, - `bfcol_11` AS `bfcol_32`, - `bfcol_12` AS `bfcol_33`, - `bfcol_13` AS `bfcol_34`, - `bfcol_14` AS `bfcol_35` + `rowindex` AS `bfcol_3`, + `rowindex` AS `bfcol_4`, + `int64_col` AS `bfcol_5`, + `string_col` AS `bfcol_6` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` +), `bfcte_1` AS ( + SELECT + `bfcol_17` AS `bfcol_23`, + `bfcol_18` AS `bfcol_24`, + `bfcol_19` AS `bfcol_25`, + `bfcol_20` AS `bfcol_26`, + `bfcol_21` AS `bfcol_27`, + `bfcol_22` AS `bfcol_28` FROM ( ( SELECT - `rowindex` AS `bfcol_9`, - `rowindex` AS `bfcol_10`, - `int64_col` AS `bfcol_11`, - `string_col` AS `bfcol_12`, - 0 AS `bfcol_13`, - ROW_NUMBER() OVER () - 1 AS `bfcol_14` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_3` AS `bfcol_17`, + `bfcol_4` AS `bfcol_18`, + `bfcol_5` AS `bfcol_19`, + `bfcol_6` AS `bfcol_20`, + 0 AS `bfcol_21`, + ROW_NUMBER() OVER () - 1 AS `bfcol_22` + FROM `bfcte_0` ) UNION ALL ( SELECT - `rowindex` AS `bfcol_24`, - `rowindex` AS `bfcol_25`, - `int64_col` AS `bfcol_26`, - `string_col` AS `bfcol_27`, - 1 AS `bfcol_28`, - ROW_NUMBER() OVER () - 1 AS `bfcol_29` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_3` AS `bfcol_11`, + `bfcol_4` AS `bfcol_12`, + `bfcol_5` AS `bfcol_13`, + `bfcol_6` AS `bfcol_14`, + 1 AS `bfcol_15`, + ROW_NUMBER() OVER () - 1 AS `bfcol_16` + FROM `bfcte_0` ) ) ) SELECT - `bfcol_30` AS `rowindex`, - `bfcol_31` AS `rowindex_1`, - `bfcol_32` AS `int64_col`, - `bfcol_33` AS `string_col` -FROM `bfcte_0` + `bfcol_23` AS `rowindex`, + `bfcol_24` AS `rowindex_1`, + `bfcol_25` AS `int64_col`, + `bfcol_26` AS `string_col` +FROM `bfcte_1` ORDER BY - `bfcol_34` ASC NULLS LAST, - `bfcol_35` ASC NULLS LAST \ No newline at end of file + `bfcol_27` ASC NULLS LAST, + `bfcol_28` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat_filter_sorted/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat_filter_sorted/out.sql index a18d6998d43..477a47036ae 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat_filter_sorted/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_concat/test_compile_concat_filter_sorted/out.sql @@ -1,55 +1,63 @@ WITH `bfcte_0` AS ( SELECT - `bfcol_6` AS `bfcol_42`, - `bfcol_7` AS `bfcol_43`, - `bfcol_8` AS `bfcol_44`, - `bfcol_9` AS `bfcol_45` + `float64_col` AS `bfcol_7`, + `int64_too` AS `bfcol_8` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + WHERE + `bool_col` +), `bfcte_1` AS ( + SELECT + `float64_col` AS `bfcol_5`, + `int64_col` AS `bfcol_6` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` +), `bfcte_2` AS ( + SELECT + `bfcol_21` AS `bfcol_33`, + `bfcol_22` AS `bfcol_34`, + `bfcol_23` AS `bfcol_35`, + `bfcol_24` AS `bfcol_36` FROM ( ( SELECT - `float64_col` AS `bfcol_6`, - `int64_col` AS `bfcol_7`, - 0 AS `bfcol_8`, - ROW_NUMBER() OVER (ORDER BY `int64_col` ASC NULLS LAST) - 1 AS `bfcol_9` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_5` AS `bfcol_21`, + `bfcol_6` AS `bfcol_22`, + 0 AS `bfcol_23`, + ROW_NUMBER() OVER (ORDER BY `bfcol_6` ASC NULLS LAST) - 1 AS `bfcol_24` + FROM `bfcte_1` ) UNION ALL ( SELECT - `float64_col` AS `bfcol_17`, - `int64_too` AS `bfcol_18`, - 1 AS `bfcol_19`, - ROW_NUMBER() OVER () - 1 AS `bfcol_20` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` - WHERE - `bool_col` + `bfcol_7` AS `bfcol_29`, + `bfcol_8` AS `bfcol_30`, + 1 AS `bfcol_31`, + ROW_NUMBER() OVER () - 1 AS `bfcol_32` + FROM `bfcte_0` ) UNION ALL ( SELECT - `float64_col` AS `bfcol_27`, - `int64_col` AS `bfcol_28`, - 2 AS `bfcol_29`, - ROW_NUMBER() OVER (ORDER BY `int64_col` ASC NULLS LAST) - 1 AS `bfcol_30` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_5` AS `bfcol_17`, + `bfcol_6` AS `bfcol_18`, + 2 AS `bfcol_19`, + ROW_NUMBER() OVER (ORDER BY `bfcol_6` ASC NULLS LAST) - 1 AS `bfcol_20` + FROM `bfcte_1` ) UNION ALL ( SELECT - `float64_col` AS `bfcol_38`, - `int64_too` AS `bfcol_39`, - 3 AS `bfcol_40`, - ROW_NUMBER() OVER () - 1 AS `bfcol_41` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` - WHERE - `bool_col` + `bfcol_7` AS `bfcol_25`, + `bfcol_8` AS `bfcol_26`, + 3 AS `bfcol_27`, + ROW_NUMBER() OVER () - 1 AS `bfcol_28` + FROM `bfcte_0` ) ) ) SELECT - `bfcol_42` AS `float64_col`, - `bfcol_43` AS `int64_col` -FROM `bfcte_0` + `bfcol_33` AS `float64_col`, + `bfcol_34` AS `int64_col` +FROM `bfcte_2` ORDER BY - `bfcol_44` ASC NULLS LAST, - `bfcol_45` ASC NULLS LAST \ No newline at end of file + `bfcol_35` ASC NULLS LAST, + `bfcol_36` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_fromrange/test_compile_fromrange/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_fromrange/test_compile_fromrange/out.sql index 47455a292b8..0b0e07056ab 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_fromrange/test_compile_fromrange/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_fromrange/test_compile_fromrange/out.sql @@ -1,165 +1,75 @@ -WITH `bfcte_6` AS ( +WITH `bfcte_0` AS ( SELECT * FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME))]) -), `bfcte_15` AS ( - SELECT - `bfcol_0` AS `bfcol_1` - FROM `bfcte_6` -), `bfcte_5` AS ( +), `bfcte_1` AS ( SELECT * - FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME))]) -), `bfcte_10` AS ( + FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME), 0, 10), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME), 1, 11), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME), 2, 12), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME), 3, 13), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME), 4, 14), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME), 5, 15), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME), 6, 16), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME), 7, 17), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME), 8, 18), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME), 9, 19), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME), 10, 20), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME), 11, 21), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME), 12, 22), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME), 13, 23), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME), 14, 24), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME), 15, 25), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME), 16, 26), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME), 17, 27), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME), 18, 28), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME), 19, 29), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME), 20, 30), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME), 21, 31), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME), 22, 32), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME), 23, 33), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME), 24, 34), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME), 25, 35), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME), 26, 36), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME), 27, 37), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME), 28, 38), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME), 29, 39)]) +), `bfcte_2` AS ( SELECT - MIN(`bfcol_2`) AS `bfcol_4` - FROM `bfcte_5` -), `bfcte_16` AS ( + `bfcol_0` AS `bfcol_4` + FROM `bfcte_0` +), `bfcte_3` AS ( SELECT - * - FROM `bfcte_10` -), `bfcte_19` AS ( + `bfcol_1` AS `bfcol_5`, + `bfcol_2` AS `bfcol_6`, + `bfcol_3` AS `bfcol_7` + FROM `bfcte_1` +), `bfcte_4` AS ( SELECT - * - FROM `bfcte_15` - CROSS JOIN `bfcte_16` -), `bfcte_21` AS ( + MIN(`bfcol_4`) AS `bfcol_8` + FROM `bfcte_2` +), `bfcte_5` AS ( SELECT - `bfcol_1`, - `bfcol_4`, + `bfcol_6` AS `bfcol_11`, + `bfcol_7` AS `bfcol_12`, CAST(FLOOR( IEEE_DIVIDE( - UNIX_MICROS(CAST(`bfcol_1` AS TIMESTAMP)) - UNIX_MICROS(CAST(CAST(`bfcol_4` AS DATE) AS TIMESTAMP)), + UNIX_MICROS(CAST(`bfcol_5` AS TIMESTAMP)) - UNIX_MICROS(CAST(CAST(`bfcol_8` AS DATE) AS TIMESTAMP)), 7000000 ) - ) AS INT64) AS `bfcol_5` - FROM `bfcte_19` -), `bfcte_23` AS ( - SELECT - MIN(`bfcol_5`) AS `bfcol_7` - FROM `bfcte_21` -), `bfcte_24` AS ( - SELECT - * - FROM `bfcte_23` -), `bfcte_4` AS ( - SELECT - * - FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME))]) -), `bfcte_13` AS ( - SELECT - `bfcol_8` AS `bfcol_9` - FROM `bfcte_4` -), `bfcte_3` AS ( - SELECT - * - FROM UNNEST(ARRAY>[STRUCT(0, CAST('2021-01-01T13:00:00' AS DATETIME), 0, 10), STRUCT(1, CAST('2021-01-01T13:00:01' AS DATETIME), 1, 11), STRUCT(2, CAST('2021-01-01T13:00:02' AS DATETIME), 2, 12), STRUCT(3, CAST('2021-01-01T13:00:03' AS DATETIME), 3, 13), STRUCT(4, CAST('2021-01-01T13:00:04' AS DATETIME), 4, 14), STRUCT(5, CAST('2021-01-01T13:00:05' AS DATETIME), 5, 15), STRUCT(6, CAST('2021-01-01T13:00:06' AS DATETIME), 6, 16), STRUCT(7, CAST('2021-01-01T13:00:07' AS DATETIME), 7, 17), STRUCT(8, CAST('2021-01-01T13:00:08' AS DATETIME), 8, 18), STRUCT(9, CAST('2021-01-01T13:00:09' AS DATETIME), 9, 19), STRUCT(10, CAST('2021-01-01T13:00:10' AS DATETIME), 10, 20), STRUCT(11, CAST('2021-01-01T13:00:11' AS DATETIME), 11, 21), STRUCT(12, CAST('2021-01-01T13:00:12' AS DATETIME), 12, 22), STRUCT(13, CAST('2021-01-01T13:00:13' AS DATETIME), 13, 23), STRUCT(14, CAST('2021-01-01T13:00:14' AS DATETIME), 14, 24), STRUCT(15, CAST('2021-01-01T13:00:15' AS DATETIME), 15, 25), STRUCT(16, CAST('2021-01-01T13:00:16' AS DATETIME), 16, 26), STRUCT(17, CAST('2021-01-01T13:00:17' AS DATETIME), 17, 27), STRUCT(18, CAST('2021-01-01T13:00:18' AS DATETIME), 18, 28), STRUCT(19, CAST('2021-01-01T13:00:19' AS DATETIME), 19, 29), STRUCT(20, CAST('2021-01-01T13:00:20' AS DATETIME), 20, 30), STRUCT(21, CAST('2021-01-01T13:00:21' AS DATETIME), 21, 31), STRUCT(22, CAST('2021-01-01T13:00:22' AS DATETIME), 22, 32), STRUCT(23, CAST('2021-01-01T13:00:23' AS DATETIME), 23, 33), STRUCT(24, CAST('2021-01-01T13:00:24' AS DATETIME), 24, 34), STRUCT(25, CAST('2021-01-01T13:00:25' AS DATETIME), 25, 35), STRUCT(26, CAST('2021-01-01T13:00:26' AS DATETIME), 26, 36), STRUCT(27, CAST('2021-01-01T13:00:27' AS DATETIME), 27, 37), STRUCT(28, CAST('2021-01-01T13:00:28' AS DATETIME), 28, 38), STRUCT(29, CAST('2021-01-01T13:00:29' AS DATETIME), 29, 39)]) -), `bfcte_9` AS ( - SELECT - MIN(`bfcol_11`) AS `bfcol_37` + ) AS INT64) AS `bfcol_13` FROM `bfcte_3` -), `bfcte_14` AS ( + CROSS JOIN `bfcte_4` +), `bfcte_6` AS ( SELECT - * - FROM `bfcte_9` -), `bfcte_18` AS ( - SELECT - * - FROM `bfcte_13` - CROSS JOIN `bfcte_14` -), `bfcte_20` AS ( - SELECT - `bfcol_9`, - `bfcol_37`, CAST(FLOOR( IEEE_DIVIDE( - UNIX_MICROS(CAST(`bfcol_9` AS TIMESTAMP)) - UNIX_MICROS(CAST(CAST(`bfcol_37` AS DATE) AS TIMESTAMP)), + UNIX_MICROS(CAST(`bfcol_4` AS TIMESTAMP)) - UNIX_MICROS(CAST(CAST(`bfcol_8` AS DATE) AS TIMESTAMP)), 7000000 ) - ) AS INT64) AS `bfcol_38` - FROM `bfcte_18` -), `bfcte_22` AS ( - SELECT - MAX(`bfcol_38`) AS `bfcol_40` - FROM `bfcte_20` -), `bfcte_25` AS ( - SELECT - * - FROM `bfcte_22` -), `bfcte_26` AS ( - SELECT - `bfcol_67` AS `bfcol_41` - FROM `bfcte_24` - CROSS JOIN `bfcte_25` - CROSS JOIN UNNEST(GENERATE_ARRAY(`bfcol_7`, `bfcol_40`, 1)) AS `bfcol_67` -), `bfcte_2` AS ( - SELECT - * - FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME))]) -), `bfcte_8` AS ( - SELECT - MIN(`bfcol_42`) AS `bfcol_44` + ) AS INT64) AS `bfcol_14` FROM `bfcte_2` -), `bfcte_27` AS ( - SELECT - * - FROM `bfcte_8` -), `bfcte_28` AS ( - SELECT - * - FROM `bfcte_26` - CROSS JOIN `bfcte_27` -), `bfcte_1` AS ( - SELECT - * - FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME), 0, 10), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME), 1, 11), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME), 2, 12), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME), 3, 13), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME), 4, 14), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME), 5, 15), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME), 6, 16), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME), 7, 17), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME), 8, 18), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME), 9, 19), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME), 10, 20), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME), 11, 21), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME), 12, 22), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME), 13, 23), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME), 14, 24), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME), 15, 25), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME), 16, 26), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME), 17, 27), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME), 18, 28), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME), 19, 29), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME), 20, 30), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME), 21, 31), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME), 22, 32), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME), 23, 33), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME), 24, 34), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME), 25, 35), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME), 26, 36), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME), 27, 37), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME), 28, 38), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME), 29, 39)]) -), `bfcte_11` AS ( - SELECT - `bfcol_45` AS `bfcol_48`, - `bfcol_46` AS `bfcol_49`, - `bfcol_47` AS `bfcol_50` - FROM `bfcte_1` -), `bfcte_0` AS ( - SELECT - * - FROM UNNEST(ARRAY>[STRUCT(CAST('2021-01-01T13:00:00' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:01' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:02' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:03' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:04' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:05' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:06' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:07' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:08' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:09' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:10' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:11' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:12' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:13' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:14' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:15' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:16' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:17' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:18' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:19' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:20' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:21' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:22' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:23' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:24' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:25' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:26' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:27' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:28' AS DATETIME)), STRUCT(CAST('2021-01-01T13:00:29' AS DATETIME))]) + CROSS JOIN `bfcte_4` ), `bfcte_7` AS ( SELECT - MIN(`bfcol_51`) AS `bfcol_53` - FROM `bfcte_0` -), `bfcte_12` AS ( - SELECT - * - FROM `bfcte_7` -), `bfcte_17` AS ( - SELECT - * - FROM `bfcte_11` - CROSS JOIN `bfcte_12` -), `bfcte_29` AS ( + MAX(`bfcol_14`) AS `bfcol_15` + FROM `bfcte_6` +), `bfcte_8` AS ( SELECT - `bfcol_49` AS `bfcol_55`, - `bfcol_50` AS `bfcol_56`, - CAST(FLOOR( - IEEE_DIVIDE( - UNIX_MICROS(CAST(`bfcol_48` AS TIMESTAMP)) - UNIX_MICROS(CAST(CAST(`bfcol_53` AS DATE) AS TIMESTAMP)), - 7000000 - ) - ) AS INT64) AS `bfcol_57` - FROM `bfcte_17` -), `bfcte_30` AS ( + MIN(`bfcol_14`) AS `bfcol_16` + FROM `bfcte_6` +), `bfcte_9` AS ( SELECT - * - FROM `bfcte_28` - LEFT JOIN `bfcte_29` - ON `bfcol_41` = `bfcol_57` + `bfcol_27` AS `bfcol_17` + FROM `bfcte_8` + CROSS JOIN `bfcte_7` + CROSS JOIN UNNEST(GENERATE_ARRAY(`bfcol_16`, `bfcol_15`, 1)) AS `bfcol_27` ) SELECT CAST(TIMESTAMP_MICROS( - CAST(CAST(`bfcol_41` AS BIGNUMERIC) * 7000000 + CAST(UNIX_MICROS(CAST(CAST(`bfcol_44` AS DATE) AS TIMESTAMP)) AS BIGNUMERIC) AS INT64) + CAST(CAST(`bfcol_17` AS BIGNUMERIC) * 7000000 + CAST(UNIX_MICROS(CAST(CAST(`bfcol_8` AS DATE) AS TIMESTAMP)) AS BIGNUMERIC) AS INT64) ) AS DATETIME) AS `bigframes_unnamed_index`, - `bfcol_55` AS `int64_col`, - `bfcol_56` AS `int64_too` -FROM `bfcte_30` + `bfcol_11` AS `int64_col`, + `bfcol_12` AS `int64_too` +FROM ( + SELECT + * + FROM `bfcte_9` + CROSS JOIN `bfcte_4` +) +LEFT JOIN `bfcte_5` + ON `bfcol_17` = `bfcol_13` ORDER BY - `bfcol_41` ASC NULLS LAST \ No newline at end of file + `bfcol_17` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin/out.sql index d80febf41ca..f4c2a494a3a 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin/out.sql @@ -1,13 +1,13 @@ -WITH `bfcte_2` AS ( - SELECT - `rowindex` AS `bfcol_2`, - `int64_col` AS `bfcol_3` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` -), `bfcte_0` AS ( +WITH `bfcte_0` AS ( SELECT `int64_too` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( + SELECT + `rowindex` AS `bfcol_3`, + `int64_col` AS `bfcol_4` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` +), `bfcte_2` AS ( SELECT `int64_too` FROM `bfcte_0` @@ -15,22 +15,21 @@ WITH `bfcte_2` AS ( `int64_too` ), `bfcte_3` AS ( SELECT - `bfcte_2`.*, - EXISTS( - SELECT - 1 - FROM ( + `int64_too` AS `bfcol_0` + FROM `bfcte_2` +), `bfcte_4` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_4`, 0) AS `bfpart1`, COALESCE(`bfcol_4`, 1) AS `bfpart2`) IN ( + ( SELECT - `int64_too` AS `bfcol_4` - FROM `bfcte_1` - ) AS `bft_1` - WHERE - COALESCE(`bfcte_2`.`bfcol_3`, 0) = COALESCE(`bft_1`.`bfcol_4`, 0) - AND COALESCE(`bfcte_2`.`bfcol_3`, 1) = COALESCE(`bft_1`.`bfcol_4`, 1) + STRUCT(COALESCE(`bfcol_0`, 0) AS `bfpart1`, COALESCE(`bfcol_0`, 1) AS `bfpart2`) + FROM `bfcte_3` + ) ) AS `bfcol_5` - FROM `bfcte_2` + FROM `bfcte_1` ) SELECT - `bfcol_2` AS `rowindex`, + `bfcol_3` AS `rowindex`, `bfcol_5` AS `int64_col` -FROM `bfcte_3` \ No newline at end of file +FROM `bfcte_4` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin_not_nullable/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin_not_nullable/out.sql index 2b2735b1637..cc1633d3a3a 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin_not_nullable/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_isin/test_compile_isin_not_nullable/out.sql @@ -1,13 +1,13 @@ -WITH `bfcte_2` AS ( - SELECT - `rowindex` AS `bfcol_2`, - `rowindex_2` AS `bfcol_3` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` -), `bfcte_0` AS ( +WITH `bfcte_0` AS ( SELECT `rowindex_2` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( + SELECT + `rowindex` AS `bfcol_3`, + `rowindex_2` AS `bfcol_4` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` +), `bfcte_2` AS ( SELECT `rowindex_2` FROM `bfcte_0` @@ -15,15 +15,19 @@ WITH `bfcte_2` AS ( `rowindex_2` ), `bfcte_3` AS ( SELECT - `bfcte_2`.*, - `bfcte_2`.`bfcol_3` IN (( + `rowindex_2` AS `bfcol_0` + FROM `bfcte_2` +), `bfcte_4` AS ( + SELECT + *, + `bfcol_4` IN (( SELECT - `rowindex_2` AS `bfcol_4` - FROM `bfcte_1` + * + FROM `bfcte_3` )) AS `bfcol_5` - FROM `bfcte_2` + FROM `bfcte_1` ) SELECT - `bfcol_2` AS `rowindex`, + `bfcol_3` AS `rowindex`, `bfcol_5` AS `rowindex_2` -FROM `bfcte_3` \ No newline at end of file +FROM `bfcte_4` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join/out.sql index dfc40840271..cac57d0c8c8 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join/out.sql @@ -1,22 +1,18 @@ WITH `bfcte_0` AS ( SELECT - `rowindex` AS `bfcol_2`, - `int64_col` AS `bfcol_3` + `int64_col` AS `bfcol_4`, + `int64_too` AS `bfcol_5` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `int64_col` AS `bfcol_6`, - `int64_too` AS `bfcol_7` + `rowindex` AS `bfcol_6`, + `int64_col` AS `bfcol_7` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` -), `bfcte_2` AS ( - SELECT - * - FROM `bfcte_0` - LEFT JOIN `bfcte_1` - ON COALESCE(`bfcol_2`, 0) = COALESCE(`bfcol_6`, 0) - AND COALESCE(`bfcol_2`, 1) = COALESCE(`bfcol_6`, 1) ) SELECT - `bfcol_3` AS `int64_col`, - `bfcol_7` AS `int64_too` -FROM `bfcte_2` \ No newline at end of file + `bfcol_7` AS `int64_col`, + `bfcol_5` AS `int64_too` +FROM `bfcte_1` +LEFT JOIN `bfcte_0` + ON COALESCE(`bfcol_6`, 0) = COALESCE(`bfcol_4`, 0) + AND COALESCE(`bfcol_6`, 1) = COALESCE(`bfcol_4`, 1) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/bool_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/bool_col/out.sql index b3a2b456737..5042f91cd95 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/bool_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/bool_col/out.sql @@ -1,23 +1,24 @@ WITH `bfcte_0` AS ( SELECT - `rowindex` AS `bfcol_2`, - `bool_col` AS `bfcol_3` + `bool_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_6`, - `bool_col` AS `bfcol_7` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_1` AS `bfcol_2`, + `bfcol_0` AS `bfcol_3` + FROM `bfcte_0` ), `bfcte_2` AS ( SELECT - * + `bfcol_1` AS `bfcol_4`, + `bfcol_0` AS `bfcol_5` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON COALESCE(CAST(`bfcol_3` AS STRING), '0') = COALESCE(CAST(`bfcol_7` AS STRING), '0') - AND COALESCE(CAST(`bfcol_3` AS STRING), '1') = COALESCE(CAST(`bfcol_7` AS STRING), '1') ) SELECT - `bfcol_2` AS `rowindex_x`, - `bfcol_3` AS `bool_col`, - `bfcol_6` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_4` AS `rowindex_x`, + `bfcol_5` AS `bool_col`, + `bfcol_2` AS `rowindex_y` +FROM `bfcte_2` +INNER JOIN `bfcte_1` + ON COALESCE(CAST(`bfcol_5` AS STRING), '0') = COALESCE(CAST(`bfcol_3` AS STRING), '0') + AND COALESCE(CAST(`bfcol_5` AS STRING), '1') = COALESCE(CAST(`bfcol_3` AS STRING), '1') \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/float64_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/float64_col/out.sql index 4abc6aa4a75..544fedadc5b 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/float64_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/float64_col/out.sql @@ -1,23 +1,24 @@ WITH `bfcte_0` AS ( SELECT - `rowindex` AS `bfcol_2`, - `float64_col` AS `bfcol_3` + `float64_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_6`, - `float64_col` AS `bfcol_7` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_1` AS `bfcol_2`, + `bfcol_0` AS `bfcol_3` + FROM `bfcte_0` ), `bfcte_2` AS ( SELECT - * + `bfcol_1` AS `bfcol_4`, + `bfcol_0` AS `bfcol_5` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON IF(IS_NAN(`bfcol_3`), 2, COALESCE(`bfcol_3`, 0)) = IF(IS_NAN(`bfcol_7`), 2, COALESCE(`bfcol_7`, 0)) - AND IF(IS_NAN(`bfcol_3`), 3, COALESCE(`bfcol_3`, 1)) = IF(IS_NAN(`bfcol_7`), 3, COALESCE(`bfcol_7`, 1)) ) SELECT - `bfcol_2` AS `rowindex_x`, - `bfcol_3` AS `float64_col`, - `bfcol_6` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_4` AS `rowindex_x`, + `bfcol_5` AS `float64_col`, + `bfcol_2` AS `rowindex_y` +FROM `bfcte_2` +INNER JOIN `bfcte_1` + ON IF(IS_NAN(`bfcol_5`), 2.0, COALESCE(`bfcol_5`, 0.0)) = IF(IS_NAN(`bfcol_3`), 2.0, COALESCE(`bfcol_3`, 0.0)) + AND IF(IS_NAN(`bfcol_5`), 3, COALESCE(`bfcol_5`, 1.0)) = IF(IS_NAN(`bfcol_3`), 3, COALESCE(`bfcol_3`, 1.0)) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/int64_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/int64_col/out.sql index b841ac1325c..05b9ceec4de 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/int64_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/int64_col/out.sql @@ -1,23 +1,24 @@ WITH `bfcte_0` AS ( SELECT - `rowindex` AS `bfcol_2`, - `int64_col` AS `bfcol_3` + `int64_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_6`, - `int64_col` AS `bfcol_7` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_1` AS `bfcol_2`, + `bfcol_0` AS `bfcol_3` + FROM `bfcte_0` ), `bfcte_2` AS ( SELECT - * + `bfcol_1` AS `bfcol_4`, + `bfcol_0` AS `bfcol_5` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON COALESCE(`bfcol_3`, 0) = COALESCE(`bfcol_7`, 0) - AND COALESCE(`bfcol_3`, 1) = COALESCE(`bfcol_7`, 1) ) SELECT - `bfcol_2` AS `rowindex_x`, - `bfcol_3` AS `int64_col`, - `bfcol_6` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_4` AS `rowindex_x`, + `bfcol_5` AS `int64_col`, + `bfcol_2` AS `rowindex_y` +FROM `bfcte_2` +INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_5`, 0) = COALESCE(`bfcol_3`, 0) + AND COALESCE(`bfcol_5`, 1) = COALESCE(`bfcol_3`, 1) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/numeric_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/numeric_col/out.sql index af2aaa69dc4..2e0114593e7 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/numeric_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/numeric_col/out.sql @@ -1,23 +1,24 @@ WITH `bfcte_0` AS ( SELECT - `rowindex` AS `bfcol_2`, - `numeric_col` AS `bfcol_3` + `numeric_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_6`, - `numeric_col` AS `bfcol_7` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` + `bfcol_1` AS `bfcol_2`, + `bfcol_0` AS `bfcol_3` + FROM `bfcte_0` ), `bfcte_2` AS ( SELECT - * + `bfcol_1` AS `bfcol_4`, + `bfcol_0` AS `bfcol_5` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON COALESCE(`bfcol_3`, CAST(0 AS NUMERIC)) = COALESCE(`bfcol_7`, CAST(0 AS NUMERIC)) - AND COALESCE(`bfcol_3`, CAST(1 AS NUMERIC)) = COALESCE(`bfcol_7`, CAST(1 AS NUMERIC)) ) SELECT - `bfcol_2` AS `rowindex_x`, - `bfcol_3` AS `numeric_col`, - `bfcol_6` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_4` AS `rowindex_x`, + `bfcol_5` AS `numeric_col`, + `bfcol_2` AS `rowindex_y` +FROM `bfcte_2` +INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_5`, CAST(0 AS NUMERIC)) = COALESCE(`bfcol_3`, CAST(0 AS NUMERIC)) + AND COALESCE(`bfcol_5`, CAST(1 AS NUMERIC)) = COALESCE(`bfcol_3`, CAST(1 AS NUMERIC)) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/string_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/string_col/out.sql index dfde2efb868..36aad503435 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/string_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/string_col/out.sql @@ -5,19 +5,15 @@ WITH `bfcte_0` AS ( FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_4`, - `string_col` AS `bfcol_5` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` -), `bfcte_2` AS ( - SELECT - * + `bfcol_0` AS `bfcol_2`, + `bfcol_1` AS `bfcol_3` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON COALESCE(CAST(`bfcol_1` AS STRING), '0') = COALESCE(CAST(`bfcol_5` AS STRING), '0') - AND COALESCE(CAST(`bfcol_1` AS STRING), '1') = COALESCE(CAST(`bfcol_5` AS STRING), '1') ) SELECT `bfcol_0` AS `rowindex_x`, `bfcol_1` AS `string_col`, - `bfcol_4` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_2` AS `rowindex_y` +FROM `bfcte_0` +INNER JOIN `bfcte_1` + ON COALESCE(CAST(`bfcol_1` AS STRING), '0') = COALESCE(CAST(`bfcol_3` AS STRING), '0') + AND COALESCE(CAST(`bfcol_1` AS STRING), '1') = COALESCE(CAST(`bfcol_3` AS STRING), '1') \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/time_col/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/time_col/out.sql index 5a858124416..b945a1cbf38 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/time_col/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_join/test_compile_join_w_on/time_col/out.sql @@ -5,19 +5,15 @@ WITH `bfcte_0` AS ( FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` ), `bfcte_1` AS ( SELECT - `rowindex` AS `bfcol_4`, - `time_col` AS `bfcol_5` - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` -), `bfcte_2` AS ( - SELECT - * + `bfcol_0` AS `bfcol_2`, + `bfcol_1` AS `bfcol_3` FROM `bfcte_0` - INNER JOIN `bfcte_1` - ON COALESCE(CAST(`bfcol_1` AS STRING), '0') = COALESCE(CAST(`bfcol_5` AS STRING), '0') - AND COALESCE(CAST(`bfcol_1` AS STRING), '1') = COALESCE(CAST(`bfcol_5` AS STRING), '1') ) SELECT `bfcol_0` AS `rowindex_x`, `bfcol_1` AS `time_col`, - `bfcol_4` AS `rowindex_y` -FROM `bfcte_2` \ No newline at end of file + `bfcol_2` AS `rowindex_y` +FROM `bfcte_0` +INNER JOIN `bfcte_1` + ON COALESCE(CAST(`bfcol_1` AS STRING), '0') = COALESCE(CAST(`bfcol_3` AS STRING), '0') + AND COALESCE(CAST(`bfcol_1` AS STRING), '1') = COALESCE(CAST(`bfcol_3` AS STRING), '1') \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_random_sample/test_compile_random_sample/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_random_sample/test_compile_random_sample/out.sql index 2f80d6ffbcc..73879aa65df 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_random_sample/test_compile_random_sample/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_random_sample/test_compile_random_sample/out.sql @@ -155,12 +155,6 @@ WITH `bfcte_0` AS ( 432000000000, 8 )]) -), `bfcte_1` AS ( - SELECT - * - FROM `bfcte_0` - WHERE - RAND() < 0.1 ) SELECT `bfcol_0` AS `bool_col`, @@ -178,6 +172,12 @@ SELECT `bfcol_12` AS `time_col`, `bfcol_13` AS `timestamp_col`, `bfcol_14` AS `duration_col` -FROM `bfcte_1` +FROM ( + SELECT + * + FROM `bfcte_0` + WHERE + RAND() < 0.1 +) ORDER BY `bfcol_15` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readtable/test_compile_readtable_w_columns_filters/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readtable/test_compile_readtable_w_columns_filters/out.sql index 512d3ca6bdd..2b71ef917d9 100644 --- a/tests/unit/core/compile/sqlglot/snapshots/test_compile_readtable/test_compile_readtable_w_columns_filters/out.sql +++ b/tests/unit/core/compile/sqlglot/snapshots/test_compile_readtable/test_compile_readtable_w_columns_filters/out.sql @@ -1,10 +1,5 @@ -WITH `bfcte_0` AS ( - SELECT - * - FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` - WHERE - `rowindex` > 0 AND `string_col` IN ('Hello, World!') -) SELECT * -FROM `bfcte_0` \ No newline at end of file +FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` +WHERE + `rowindex` > 0 AND `string_col` IN ('Hello, World!') \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/snapshots/test_dataframe_accessor/test_sql_scalar/out.sql b/tests/unit/core/compile/sqlglot/snapshots/test_dataframe_accessor/test_sql_scalar/out.sql new file mode 100644 index 00000000000..14853067c70 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/snapshots/test_dataframe_accessor/test_sql_scalar/out.sql @@ -0,0 +1,4 @@ +SELECT + `rowindex`, + ROUND(`int64_col` + `int64_too`) AS `0` +FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` AS `bft_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_all_options/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_all_options/out.sql new file mode 100644 index 00000000000..781019a0680 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_all_options/out.sql @@ -0,0 +1,10 @@ +LOAD DATA OVERWRITE INTO `my-project.my_dataset.my_table` ( + `col1` INT64, + `col2` STRING +) PARTITION BY `date_col` CLUSTER BY + `cluster_col` OPTIONS ( + description='my table' +) FROM FILES (format='CSV', uris=['gs://bucket/path*']) WITH PARTITION COLUMNS ( + `part1` DATE, + `part2` STRING +) WITH CONNECTION `my-connection` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_minimal/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_minimal/out.sql new file mode 100644 index 00000000000..c5f66003257 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_ddl/test_load_data_minimal/out.sql @@ -0,0 +1 @@ +LOAD DATA INTO `my-project.my_dataset.my_table` FROM FILES (format='CSV', uris=['gs://bucket/path*']) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_select/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_select/out.sql new file mode 100644 index 00000000000..e2e9225c9f7 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_select/out.sql @@ -0,0 +1,6 @@ +INSERT INTO `bigframes-dev`.`sqlglot_test`.`dest_table` +( + SELECT + * + FROM `source_table` +) \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_table/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_table/out.sql new file mode 100644 index 00000000000..2486d8d0a3b --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_insert_from_table/out.sql @@ -0,0 +1,2 @@ +INSERT INTO `bigframes-dev`.`sqlglot_test`.`dest_table` +`source_table` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_select/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_select/out.sql new file mode 100644 index 00000000000..c4f43f390ed --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_select/out.sql @@ -0,0 +1,9 @@ +MERGE INTO `bigframes-dev`.`sqlglot_test`.`dest_table` +USING ( + SELECT + * + FROM `source_table` +) +ON FALSE +WHEN NOT MATCHED BY SOURCE THEN DELETE +WHEN NOT MATCHED THEN INSERT ROW \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_table/out.sql b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_table/out.sql new file mode 100644 index 00000000000..bfc1532ca2d --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/snapshots/test_dml/test_replace_from_table/out.sql @@ -0,0 +1,5 @@ +MERGE INTO `bigframes-dev`.`sqlglot_test`.`dest_table` +USING `source_table` +ON FALSE +WHEN NOT MATCHED BY SOURCE THEN DELETE +WHEN NOT MATCHED THEN INSERT ROW \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/sql/test_base.py b/tests/unit/core/compile/sqlglot/sql/test_base.py new file mode 100644 index 00000000000..5ba77d925d0 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -0,0 +1,161 @@ +# Copyright 2024 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 datetime +import decimal +import re + +import numpy as np +import pandas as pd +import pyarrow as pa +import pytest +import shapely.geometry # type: ignore + +import bigframes.core.compile.sqlglot.sql.base as sql + + +@pytest.mark.parametrize( + ("value", "expected_pattern"), + ( + pytest.param(None, "NULL", id="null"), + pytest.param(True, "TRUE", id="true"), + pytest.param(False, "FALSE", id="false"), + pytest.param(123, "123", id="int"), + pytest.param(123.75, "123.75", id="float"), + pytest.param("abc", "'abc'", id="string"), + pytest.param( + b"\x01\x02\x03ABC", "CAST(b'\\x01\\x02\\x03ABC' AS BYTES)", id="bytes" + ), + pytest.param( + decimal.Decimal("123.75"), "CAST(123.75 AS NUMERIC)", id="decimal" + ), + pytest.param( + datetime.date(2025, 1, 1), "CAST('2025-01-01' AS DATE)", id="date" + ), + pytest.param( + datetime.datetime(2025, 1, 2, 3, 45, 6, 789123), + "CAST('2025-01-02T03:45:06.789123' AS DATETIME)", + id="datetime", + ), + pytest.param( + datetime.time(12, 34, 56, 789123), + "CAST('12:34:56.789123' AS TIME)", + id="time", + ), + pytest.param( + datetime.datetime( + 2025, 1, 2, 3, 45, 6, 789123, tzinfo=datetime.timezone.utc + ), + "CAST('2025-01-02T03:45:06.789123+00:00' AS TIMESTAMP)", + id="timestamp", + ), + pytest.param(np.int64(123), "123", id="np_int64"), + pytest.param(np.float64(123.75), "123.75", id="np_float64"), + pytest.param(float("inf"), "CAST('Infinity' AS FLOAT64)", id="inf"), + pytest.param(float("-inf"), "CAST('-Infinity' AS FLOAT64)", id="neg_inf"), + pytest.param(float("nan"), "NULL", id="nan"), + pytest.param(pd.NA, "NULL", id="pd_na"), + pytest.param(datetime.timedelta(seconds=1), "1000000", id="timedelta"), + pytest.param("POINT (0 1)", "'POINT (0 1)'", id="string_geo"), + ), +) +def test_literal(value, expected_pattern): + got = sql.to_sql(sql.literal(value)) + assert got == expected_pattern + + +def test_literal_for_geo(): + value = shapely.geometry.Point(0, 1) + expected_pattern = r"ST_GEOGFROMTEXT\('POINT \(0[.]?0* 1[.]?0*\)'\)" + got = sql.to_sql(sql.literal(value)) + assert re.match(expected_pattern, got) is not None + + +@pytest.mark.parametrize( + ("value", "dtype", "expected"), + ( + pytest.param( + decimal.Decimal("1.23"), + sql.dtypes.BIGNUMERIC_DTYPE, + "CAST(1.23 AS BIGNUMERIC)", + id="bignumeric", + ), + pytest.param( + [], + pd.ArrowDtype(pa.list_(pa.int64())), + "ARRAY[]", + id="empty_array", + ), + pytest.param( + {"a": 1, "b": "hello"}, + pd.ArrowDtype(pa.struct([("a", pa.int64()), ("b", pa.string())])), + "STRUCT(1 AS `a`, 'hello' AS `b`)", + id="struct", + ), + pytest.param( + float("nan"), + sql.dtypes.FLOAT_DTYPE, + "CAST('NaN' AS FLOAT64)", + id="explicit_nan", + ), + pytest.param( + pa.scalar(123, type=pa.int64()), + None, + "123", + id="pa_scalar_int", + ), + pytest.param( + pa.scalar(None, type=pa.int64()), + None, + "CAST(NULL AS INT64)", + id="pa_scalar_null", + ), + pytest.param( + {"a": 10}, + sql.dtypes.JSON_DTYPE, + "PARSE_JSON('{\\'a\\': 10}')", + id="json", + ), + ), +) +def test_literal_explicit_dtype(value, dtype, expected): + got = sql.to_sql(sql.literal(value, dtype=dtype)) + assert got == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + pytest.param([True, False], "[TRUE, FALSE]", id="bool"), + pytest.param([123, 456], "[123, 456]", id="int"), + pytest.param( + [123.75, 456.78, float("nan"), float("inf"), float("-inf")], + "[\n 123.75,\n 456.78,\n CAST('NaN' AS FLOAT64),\n CAST('Infinity' AS FLOAT64),\n CAST('-Infinity' AS FLOAT64)\n]", + id="float", + ), + pytest.param( + [b"\x01\x02\x03ABC", b"\x01\x02\x03ABC"], + "[CAST(b'\\x01\\x02\\x03ABC' AS BYTES), CAST(b'\\x01\\x02\\x03ABC' AS BYTES)]", + id="bytes", + ), + pytest.param( + [datetime.date(2025, 1, 1), datetime.date(2025, 1, 1)], + "[CAST('2025-01-01' AS DATE), CAST('2025-01-01' AS DATE)]", + id="date", + ), + ), +) +def test_literal_for_list(value: list, expected: str): + got = sql.to_sql(sql.literal(value)) + assert got == expected diff --git a/tests/unit/core/compile/sqlglot/sql/test_ddl.py b/tests/unit/core/compile/sqlglot/sql/test_ddl.py new file mode 100644 index 00000000000..14d3708883d --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/test_ddl.py @@ -0,0 +1,42 @@ +# Copyright 2026 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.core.compile.sqlglot.sql as sql + +pytest.importorskip("pytest_snapshot") + + +def test_load_data_minimal(snapshot): + expr = sql.load_data( + "my-project.my_dataset.my_table", + from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, + ) + snapshot.assert_match(sql.to_sql(expr), "out.sql") + + +def test_load_data_all_options(snapshot): + expr = sql.load_data( + "my-project.my_dataset.my_table", + write_disposition="OVERWRITE", + columns={"col1": "INT64", "col2": "STRING"}, + partition_by=["date_col"], + cluster_by=["cluster_col"], + table_options={"description": "my table"}, + from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, + with_partition_columns={"part1": "DATE", "part2": "STRING"}, + connection_name="my-connection", + ) + snapshot.assert_match(sql.to_sql(expr), "out.sql") diff --git a/tests/unit/core/compile/sqlglot/sql/test_dml.py b/tests/unit/core/compile/sqlglot/sql/test_dml.py new file mode 100644 index 00000000000..946efd21bdd --- /dev/null +++ b/tests/unit/core/compile/sqlglot/sql/test_dml.py @@ -0,0 +1,73 @@ +# Copyright 2026 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 bigframes_vendored.sqlglot.expressions as sge +from google.cloud import bigquery +import pytest + +from bigframes.core.compile.sqlglot.sql import base, dml + +pytest.importorskip("pytest_snapshot") + + +def test_insert_from_select(snapshot): + query = sge.select("*").from_( + sge.Table(this=sge.Identifier(this="source_table", quoted=True)) + ) + destination = bigquery.TableReference.from_string( + "bigframes-dev.sqlglot_test.dest_table" + ) + + expr = dml.insert(query, destination) + sql = base.to_sql(expr) + + snapshot.assert_match(sql, "out.sql") + + +def test_insert_from_table(snapshot): + query = sge.Table(this=sge.Identifier(this="source_table", quoted=True)) + destination = bigquery.TableReference.from_string( + "bigframes-dev.sqlglot_test.dest_table" + ) + + expr = dml.insert(query, destination) + sql = base.to_sql(expr) + + snapshot.assert_match(sql, "out.sql") + + +def test_replace_from_select(snapshot): + query = sge.select("*").from_( + sge.Table(this=sge.Identifier(this="source_table", quoted=True)) + ) + destination = bigquery.TableReference.from_string( + "bigframes-dev.sqlglot_test.dest_table" + ) + + expr = dml.replace(query, destination) + sql = base.to_sql(expr) + + snapshot.assert_match(sql, "out.sql") + + +def test_replace_from_table(snapshot): + query = sge.Table(this=sge.Identifier(this="source_table", quoted=True)) + destination = bigquery.TableReference.from_string( + "bigframes-dev.sqlglot_test.dest_table" + ) + + expr = dml.replace(query, destination) + sql = base.to_sql(expr) + + snapshot.assert_match(sql, "out.sql") diff --git a/tests/unit/core/compile/sqlglot/test_dataframe_accessor.py b/tests/unit/core/compile/sqlglot/test_dataframe_accessor.py new file mode 100644 index 00000000000..327b8e4206a --- /dev/null +++ b/tests/unit/core/compile/sqlglot/test_dataframe_accessor.py @@ -0,0 +1,45 @@ +# Copyright 2026 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 unittest.mock as mock + +import pandas as pd +import pytest + +import bigframes.pandas as bpd +import bigframes.session + +pytest.importorskip("pytest_snapshot") + + +def test_sql_scalar(scalar_types_df: bpd.DataFrame, snapshot, monkeypatch): + session = mock.create_autospec(bigframes.session.Session) + session.read_pandas.return_value = scalar_types_df + + def to_pandas(series, *, ordered): + assert ordered is True + sql, _, _ = series.to_frame()._to_sql_query(include_index=True) + return sql + + monkeypatch.setattr(bpd.Series, "to_pandas", to_pandas) + + df = pd.DataFrame({"int64_col": [1, 2], "int64_too": [3, 4]}) + result = df.bigquery.sql_scalar( + "ROUND({int64_col} + {int64_too})", + output_dtype=pd.Int64Dtype(), + session=session, + ) + + session.read_pandas.assert_called_once() + snapshot.assert_match(result, "out.sql") diff --git a/tests/unit/core/compile/sqlglot/tpch/conftest.py b/tests/unit/core/compile/sqlglot/tpch/conftest.py new file mode 100644 index 00000000000..c42f9a8ddf6 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/conftest.py @@ -0,0 +1,148 @@ +# Copyright 2026 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 datetime +import unittest.mock as mock + +from google.cloud import bigquery +import pytest + +import bigframes.testing.mocks as mocks + +freezegun = pytest.importorskip("freezegun") + +TPCH_SCHEMAS = { + "LINEITEM": [ + bigquery.SchemaField("L_ORDERKEY", "INTEGER"), + bigquery.SchemaField("L_PARTKEY", "INTEGER"), + bigquery.SchemaField("L_SUPPKEY", "INTEGER"), + bigquery.SchemaField("L_LINENUMBER", "INTEGER"), + bigquery.SchemaField("L_QUANTITY", "FLOAT"), + bigquery.SchemaField("L_EXTENDEDPRICE", "FLOAT"), + bigquery.SchemaField("L_DISCOUNT", "FLOAT"), + bigquery.SchemaField("L_TAX", "FLOAT"), + bigquery.SchemaField("L_RETURNFLAG", "STRING"), + bigquery.SchemaField("L_LINESTATUS", "STRING"), + bigquery.SchemaField("L_SHIPDATE", "DATE"), + bigquery.SchemaField("L_COMMITDATE", "DATE"), + bigquery.SchemaField("L_RECEIPTDATE", "DATE"), + bigquery.SchemaField("L_SHIPINSTRUCT", "STRING"), + bigquery.SchemaField("L_SHIPMODE", "STRING"), + bigquery.SchemaField("L_COMMENT", "STRING"), + ], + "ORDERS": [ + bigquery.SchemaField("O_ORDERKEY", "INTEGER"), + bigquery.SchemaField("O_CUSTKEY", "INTEGER"), + bigquery.SchemaField("O_ORDERSTATUS", "STRING"), + bigquery.SchemaField("O_TOTALPRICE", "FLOAT"), + bigquery.SchemaField("O_ORDERDATE", "DATE"), + bigquery.SchemaField("O_ORDERPRIORITY", "STRING"), + bigquery.SchemaField("O_CLERK", "STRING"), + bigquery.SchemaField("O_SHIPPRIORITY", "INTEGER"), + bigquery.SchemaField("O_COMMENT", "STRING"), + ], + "PART": [ + bigquery.SchemaField("P_PARTKEY", "INTEGER"), + bigquery.SchemaField("P_NAME", "STRING"), + bigquery.SchemaField("P_MFGR", "STRING"), + bigquery.SchemaField("P_BRAND", "STRING"), + bigquery.SchemaField("P_TYPE", "STRING"), + bigquery.SchemaField("P_SIZE", "INTEGER"), + bigquery.SchemaField("P_CONTAINER", "STRING"), + bigquery.SchemaField("P_RETAILPRICE", "FLOAT"), + bigquery.SchemaField("P_COMMENT", "STRING"), + ], + "SUPPLIER": [ + bigquery.SchemaField("S_SUPPKEY", "INTEGER"), + bigquery.SchemaField("S_NAME", "STRING"), + bigquery.SchemaField("S_ADDRESS", "STRING"), + bigquery.SchemaField("S_NATIONKEY", "INTEGER"), + bigquery.SchemaField("S_PHONE", "STRING"), + bigquery.SchemaField("S_ACCTBAL", "FLOAT"), + bigquery.SchemaField("S_COMMENT", "STRING"), + ], + "PARTSUPP": [ + bigquery.SchemaField("PS_PARTKEY", "INTEGER"), + bigquery.SchemaField("PS_SUPPKEY", "INTEGER"), + bigquery.SchemaField("PS_AVAILQTY", "INTEGER"), + bigquery.SchemaField("PS_SUPPLYCOST", "FLOAT"), + bigquery.SchemaField("PS_COMMENT", "STRING"), + ], + "CUSTOMER": [ + bigquery.SchemaField("C_CUSTKEY", "INTEGER"), + bigquery.SchemaField("C_NAME", "STRING"), + bigquery.SchemaField("C_ADDRESS", "STRING"), + bigquery.SchemaField("C_NATIONKEY", "INTEGER"), + bigquery.SchemaField("C_PHONE", "STRING"), + bigquery.SchemaField("C_ACCTBAL", "FLOAT"), + bigquery.SchemaField("C_MKTSEGMENT", "STRING"), + bigquery.SchemaField("C_COMMENT", "STRING"), + ], + "NATION": [ + bigquery.SchemaField("N_NATIONKEY", "INTEGER"), + bigquery.SchemaField("N_NAME", "STRING"), + bigquery.SchemaField("N_REGIONKEY", "INTEGER"), + bigquery.SchemaField("N_COMMENT", "STRING"), + ], + "REGION": [ + bigquery.SchemaField("R_REGIONKEY", "INTEGER"), + bigquery.SchemaField("R_NAME", "STRING"), + bigquery.SchemaField("R_COMMENT", "STRING"), + ], +} + + +@pytest.fixture(scope="session") +def tpch_session(): + from bigframes.testing import compiler_session + + anonymous_dataset = bigquery.DatasetReference.from_string("bigframes-dev.tpch") + location = "us-central1" + + with freezegun.freeze_time("2026-03-10 18:00:00"): + session = mocks.create_bigquery_session( + anonymous_dataset=anonymous_dataset, + location=location, + ) + + def get_table_mock(table_ref): + if isinstance(table_ref, str): + table_ref = bigquery.TableReference.from_string(table_ref) + + table_id = table_ref.table_id + schema = TPCH_SCHEMAS.get(table_id, []) + + table = mock.create_autospec(bigquery.Table, instance=True) + table._properties = {} + # mocks.create_bigquery_session's CURRENT_TIMESTAMP() returns offset-naive datetime.now() + # So we should also use offset-naive here to avoid comparison errors. + now = datetime.datetime.now() + type(table).schema = mock.PropertyMock(return_value=schema) + type(table).project = table_ref.project + type(table).dataset_id = table_ref.dataset_id + type(table).table_id = table_id + type(table).num_rows = mock.PropertyMock(return_value=1000000) + type(table).num_bytes = mock.PropertyMock(return_value=1000000) + type(table).location = mock.PropertyMock(return_value=location) + type(table).table_type = mock.PropertyMock(return_value="TABLE") + type(table).created = mock.PropertyMock(return_value=now) + type(table).modified = mock.PropertyMock(return_value=now) + type(table).range_partitioning = mock.PropertyMock(return_value=None) + type(table).time_partitioning = mock.PropertyMock(return_value=None) + type(table).clustering_fields = mock.PropertyMock(return_value=None) + return table + + session.bqclient.get_table.side_effect = get_table_mock + session._executor = compiler_session.SQLCompilerExecutor() + return session diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/1/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/1/out.sql new file mode 100644 index 00000000000..1afccf820c1 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/1/out.sql @@ -0,0 +1,77 @@ +WITH `bfcte_0` AS ( + SELECT + `L_QUANTITY`, + `L_EXTENDEDPRICE`, + `L_DISCOUNT`, + `L_TAX`, + `L_RETURNFLAG`, + `L_LINESTATUS`, + `L_SHIPDATE`, + `L_QUANTITY` AS `bfcol_7`, + `L_EXTENDEDPRICE` AS `bfcol_8`, + `L_DISCOUNT` AS `bfcol_9`, + `L_TAX` AS `bfcol_10`, + `L_RETURNFLAG` AS `bfcol_11`, + `L_LINESTATUS` AS `bfcol_12`, + `L_SHIPDATE` <= CAST('1998-09-02' AS DATE) AS `bfcol_13`, + `L_QUANTITY` AS `bfcol_27`, + `L_EXTENDEDPRICE` AS `bfcol_28`, + `L_DISCOUNT` AS `bfcol_29`, + `L_TAX` AS `bfcol_30`, + `L_RETURNFLAG` AS `bfcol_31`, + `L_LINESTATUS` AS `bfcol_32`, + `L_EXTENDEDPRICE` * ( + 1.0 - `L_DISCOUNT` + ) AS `bfcol_33`, + `L_QUANTITY` AS `bfcol_41`, + `L_EXTENDEDPRICE` AS `bfcol_42`, + `L_DISCOUNT` AS `bfcol_43`, + `L_RETURNFLAG` AS `bfcol_44`, + `L_LINESTATUS` AS `bfcol_45`, + `L_EXTENDEDPRICE` * ( + 1.0 - `L_DISCOUNT` + ) AS `bfcol_46`, + ( + `L_EXTENDEDPRICE` * ( + 1.0 - `L_DISCOUNT` + ) + ) * ( + 1.0 + `L_TAX` + ) AS `bfcol_47` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `L_SHIPDATE` <= CAST('1998-09-02' AS DATE) +), `bfcte_1` AS ( + SELECT + `bfcol_44`, + `bfcol_45`, + COALESCE(SUM(`bfcol_41`), 0) AS `bfcol_55`, + COALESCE(SUM(`bfcol_42`), 0) AS `bfcol_56`, + COALESCE(SUM(`bfcol_46`), 0) AS `bfcol_57`, + COALESCE(SUM(`bfcol_47`), 0) AS `bfcol_58`, + AVG(`bfcol_41`) AS `bfcol_59`, + AVG(`bfcol_42`) AS `bfcol_60`, + AVG(`bfcol_43`) AS `bfcol_61`, + COUNT(`bfcol_41`) AS `bfcol_62` + FROM `bfcte_0` + WHERE + NOT `bfcol_44` IS NULL AND NOT `bfcol_45` IS NULL + GROUP BY + `bfcol_44`, + `bfcol_45` +) +SELECT + `bfcol_44` AS `L_RETURNFLAG`, + `bfcol_45` AS `L_LINESTATUS`, + `bfcol_55` AS `SUM_QTY`, + `bfcol_56` AS `SUM_BASE_PRICE`, + `bfcol_57` AS `SUM_DISC_PRICE`, + `bfcol_58` AS `SUM_CHARGE`, + `bfcol_59` AS `AVG_QTY`, + `bfcol_60` AS `AVG_PRICE`, + `bfcol_61` AS `AVG_DISC`, + `bfcol_62` AS `COUNT_ORDER` +FROM `bfcte_1` +ORDER BY + `bfcol_44` ASC NULLS LAST, + `bfcol_45` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/10/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/10/out.sql new file mode 100644 index 00000000000..8362d3afca1 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/10/out.sql @@ -0,0 +1,171 @@ +WITH `bfcte_0` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_0`, + `N_NAME` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_2`, + `L_EXTENDEDPRICE` AS `bfcol_3`, + `L_DISCOUNT` AS `bfcol_4`, + `L_RETURNFLAG` AS `bfcol_5` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_6`, + `O_CUSTKEY` AS `bfcol_7`, + `O_ORDERDATE` AS `bfcol_8` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_9`, + `C_NAME` AS `bfcol_10`, + `C_ADDRESS` AS `bfcol_11`, + `C_NATIONKEY` AS `bfcol_12`, + `C_PHONE` AS `bfcol_13`, + `C_ACCTBAL` AS `bfcol_14`, + `C_COMMENT` AS `bfcol_15` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `bfcol_9` AS `bfcol_16`, + `bfcol_10` AS `bfcol_17`, + `bfcol_11` AS `bfcol_18`, + `bfcol_12` AS `bfcol_19`, + `bfcol_13` AS `bfcol_20`, + `bfcol_14` AS `bfcol_21`, + `bfcol_15` AS `bfcol_22`, + `bfcol_6` AS `bfcol_23`, + `bfcol_8` AS `bfcol_24` + FROM `bfcte_3` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_9`, 0) = COALESCE(`bfcol_7`, 0) + AND COALESCE(`bfcol_9`, 1) = COALESCE(`bfcol_7`, 1) +), `bfcte_5` AS ( + SELECT + `bfcol_16` AS `bfcol_25`, + `bfcol_17` AS `bfcol_26`, + `bfcol_18` AS `bfcol_27`, + `bfcol_19` AS `bfcol_28`, + `bfcol_20` AS `bfcol_29`, + `bfcol_21` AS `bfcol_30`, + `bfcol_22` AS `bfcol_31`, + `bfcol_24` AS `bfcol_32`, + `bfcol_3` AS `bfcol_33`, + `bfcol_4` AS `bfcol_34`, + `bfcol_5` AS `bfcol_35` + FROM `bfcte_4` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_23`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_23`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_6` AS ( + SELECT + `bfcol_25`, + `bfcol_26`, + `bfcol_27`, + `bfcol_28`, + `bfcol_29`, + `bfcol_30`, + `bfcol_31`, + `bfcol_32`, + `bfcol_33`, + `bfcol_34`, + `bfcol_35`, + `bfcol_0`, + `bfcol_1`, + `bfcol_25` AS `bfcol_47`, + `bfcol_26` AS `bfcol_48`, + `bfcol_27` AS `bfcol_49`, + `bfcol_29` AS `bfcol_50`, + `bfcol_30` AS `bfcol_51`, + `bfcol_31` AS `bfcol_52`, + `bfcol_33` AS `bfcol_53`, + `bfcol_34` AS `bfcol_54`, + `bfcol_1` AS `bfcol_55`, + ( + ( + `bfcol_32` >= CAST('1993-10-01' AS DATE) + ) + AND ( + `bfcol_32` < CAST('1994-01-01' AS DATE) + ) + ) + AND ( + `bfcol_35` = 'R' + ) AS `bfcol_56`, + `bfcol_25` AS `bfcol_76`, + `bfcol_26` AS `bfcol_77`, + `bfcol_27` AS `bfcol_78`, + `bfcol_29` AS `bfcol_79`, + `bfcol_30` AS `bfcol_80`, + `bfcol_31` AS `bfcol_81`, + `bfcol_1` AS `bfcol_82`, + ROUND(( + `bfcol_33` * ( + 1 - `bfcol_34` + ) + ), 2) AS `bfcol_83` + FROM `bfcte_5` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_28`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_28`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + ( + `bfcol_32` >= CAST('1993-10-01' AS DATE) + ) + AND ( + `bfcol_32` < CAST('1994-01-01' AS DATE) + ) + ) + AND ( + `bfcol_35` = 'R' + ) +), `bfcte_7` AS ( + SELECT + `bfcol_76`, + `bfcol_77`, + `bfcol_80`, + `bfcol_79`, + `bfcol_82`, + `bfcol_78`, + `bfcol_81`, + COALESCE(SUM(`bfcol_83`), 0) AS `bfcol_92` + FROM `bfcte_6` + WHERE + NOT `bfcol_76` IS NULL + AND NOT `bfcol_77` IS NULL + AND NOT `bfcol_80` IS NULL + AND NOT `bfcol_79` IS NULL + AND NOT `bfcol_82` IS NULL + AND NOT `bfcol_78` IS NULL + AND NOT `bfcol_81` IS NULL + GROUP BY + `bfcol_76`, + `bfcol_77`, + `bfcol_80`, + `bfcol_79`, + `bfcol_82`, + `bfcol_78`, + `bfcol_81` +) +SELECT + `bfcol_76` AS `C_CUSTKEY`, + `bfcol_77` AS `C_NAME`, + `bfcol_92` AS `REVENUE`, + `bfcol_80` AS `C_ACCTBAL`, + `bfcol_82` AS `N_NAME`, + `bfcol_78` AS `C_ADDRESS`, + `bfcol_79` AS `C_PHONE`, + `bfcol_81` AS `C_COMMENT` +FROM `bfcte_7` +ORDER BY + `bfcol_92` DESC, + `bfcol_76` ASC NULLS LAST, + `bfcol_77` ASC NULLS LAST, + `bfcol_80` ASC NULLS LAST, + `bfcol_79` ASC NULLS LAST, + `bfcol_82` ASC NULLS LAST, + `bfcol_78` ASC NULLS LAST, + `bfcol_81` ASC NULLS LAST +LIMIT 20 \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/11/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/11/out.sql new file mode 100644 index 00000000000..2b296be7448 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/11/out.sql @@ -0,0 +1,122 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT(0.0, 0, 0)]) +), `bfcte_1` AS ( + SELECT + `PS_SUPPKEY` AS `bfcol_0`, + `PS_AVAILQTY` AS `bfcol_1`, + `PS_SUPPLYCOST` AS `bfcol_2` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `PS_PARTKEY` AS `bfcol_10`, + `PS_SUPPKEY` AS `bfcol_11`, + `PS_AVAILQTY` AS `bfcol_12`, + `PS_SUPPLYCOST` AS `bfcol_13` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_3`, + `S_NATIONKEY` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_18` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `N_NAME` = 'GERMANY' +), `bfcte_5` AS ( + SELECT + `bfcol_3` AS `bfcol_19` + FROM `bfcte_4` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_18`, 0) = COALESCE(`bfcol_4`, 0) + AND COALESCE(`bfcol_18`, 1) = COALESCE(`bfcol_4`, 1) +), `bfcte_6` AS ( + SELECT + `bfcol_19`, + `bfcol_0`, + `bfcol_1`, + `bfcol_2`, + `bfcol_1` AS `bfcol_25`, + `bfcol_2` AS `bfcol_26`, + `bfcol_2` AS `bfcol_33`, + `bfcol_1` AS `bfcol_34`, + `bfcol_2` * `bfcol_1` AS `bfcol_40` + FROM `bfcte_5` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_19`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_19`, 1) = COALESCE(`bfcol_0`, 1) +), `bfcte_7` AS ( + SELECT + `bfcol_19`, + `bfcol_10`, + `bfcol_11`, + `bfcol_12`, + `bfcol_13`, + `bfcol_10` AS `bfcol_27`, + `bfcol_13` * `bfcol_12` AS `bfcol_28` + FROM `bfcte_5` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_19`, 0) = COALESCE(`bfcol_11`, 0) + AND COALESCE(`bfcol_19`, 1) = COALESCE(`bfcol_11`, 1) +), `bfcte_8` AS ( + SELECT + COALESCE(SUM(`bfcol_40`), 0) AS `bfcol_44` + FROM `bfcte_6` +), `bfcte_9` AS ( + SELECT + `bfcol_27`, + COALESCE(SUM(`bfcol_28`), 0) AS `bfcol_35` + FROM `bfcte_7` + WHERE + NOT `bfcol_27` IS NULL + GROUP BY + `bfcol_27` +), `bfcte_10` AS ( + SELECT + `bfcol_44`, + 0 AS `bfcol_45` + FROM `bfcte_8` +), `bfcte_11` AS ( + SELECT + `bfcol_27` AS `bfcol_41`, + ROUND(`bfcol_35`, 2) AS `bfcol_42` + FROM `bfcte_9` +), `bfcte_12` AS ( + SELECT + `bfcol_7`, + `bfcol_8`, + `bfcol_9`, + `bfcol_44`, + `bfcol_45`, + CASE WHEN `bfcol_9` = 0 THEN `bfcol_44` END AS `bfcol_46`, + IF(`bfcol_45` = 0, CASE WHEN `bfcol_9` = 0 THEN `bfcol_44` END, NULL) AS `bfcol_51` + FROM `bfcte_0` + CROSS JOIN `bfcte_10` +), `bfcte_13` AS ( + SELECT + `bfcol_7`, + `bfcol_8`, + ANY_VALUE(`bfcol_51`) AS `bfcol_55` + FROM `bfcte_12` + WHERE + NOT `bfcol_7` IS NULL AND NOT `bfcol_8` IS NULL + GROUP BY + `bfcol_7`, + `bfcol_8` +), `bfcte_14` AS ( + SELECT + `bfcol_55` * 0.0001 AS `bfcol_58` + FROM `bfcte_13` +) +SELECT + `bfcol_41` AS `PS_PARTKEY`, + `bfcol_42` AS `VALUE` +FROM `bfcte_11` +CROSS JOIN `bfcte_14` +WHERE + `bfcol_42` > `bfcol_58` +ORDER BY + `bfcol_42` DESC \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/12/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/12/out.sql new file mode 100644 index 00000000000..4d91dcdac19 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/12/out.sql @@ -0,0 +1,93 @@ +WITH `bfcte_0` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_0`, + `L_SHIPDATE` AS `bfcol_1`, + `L_COMMITDATE` AS `bfcol_2`, + `L_RECEIPTDATE` AS `bfcol_3`, + `L_SHIPMODE` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_5`, + `O_ORDERPRIORITY` AS `bfcol_6` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `bfcol_5`, + `bfcol_6`, + `bfcol_0`, + `bfcol_1`, + `bfcol_2`, + `bfcol_3`, + `bfcol_4`, + `bfcol_6` AS `bfcol_12`, + `bfcol_4` AS `bfcol_13`, + ( + ( + ( + COALESCE(COALESCE(`bfcol_4` IN ('MAIL', 'SHIP'), FALSE), FALSE) + AND ( + `bfcol_2` < `bfcol_3` + ) + ) + AND ( + `bfcol_1` < `bfcol_2` + ) + ) + AND ( + `bfcol_3` >= CAST('1994-01-01' AS DATE) + ) + ) + AND ( + `bfcol_3` < CAST('1995-01-01' AS DATE) + ) AS `bfcol_14`, + `bfcol_6` AS `bfcol_20`, + `bfcol_4` AS `bfcol_21`, + CAST(COALESCE(COALESCE(`bfcol_6` IN ('1-URGENT', '2-HIGH'), FALSE), FALSE) AS INT64) AS `bfcol_22`, + `bfcol_4` AS `bfcol_26`, + CAST(COALESCE(COALESCE(`bfcol_6` IN ('1-URGENT', '2-HIGH'), FALSE), FALSE) AS INT64) AS `bfcol_27`, + CAST(NOT ( + COALESCE(COALESCE(`bfcol_6` IN ('1-URGENT', '2-HIGH'), FALSE), FALSE) + ) AS INT64) AS `bfcol_28` + FROM `bfcte_1` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_5`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_5`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + ( + ( + COALESCE(COALESCE(`bfcol_4` IN ('MAIL', 'SHIP'), FALSE), FALSE) + AND ( + `bfcol_2` < `bfcol_3` + ) + ) + AND ( + `bfcol_1` < `bfcol_2` + ) + ) + AND ( + `bfcol_3` >= CAST('1994-01-01' AS DATE) + ) + ) + AND ( + `bfcol_3` < CAST('1995-01-01' AS DATE) + ) +), `bfcte_3` AS ( + SELECT + `bfcol_26`, + COALESCE(SUM(`bfcol_27`), 0) AS `bfcol_32`, + COALESCE(SUM(`bfcol_28`), 0) AS `bfcol_33` + FROM `bfcte_2` + WHERE + NOT `bfcol_26` IS NULL + GROUP BY + `bfcol_26` +) +SELECT + `bfcol_26` AS `L_SHIPMODE`, + `bfcol_32` AS `HIGH_LINE_COUNT`, + `bfcol_33` AS `LOW_LINE_COUNT` +FROM `bfcte_3` +ORDER BY + `bfcol_26` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/13/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/13/out.sql new file mode 100644 index 00000000000..728738a15e0 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/13/out.sql @@ -0,0 +1,42 @@ +WITH `bfcte_0` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_10`, + `O_CUSTKEY` AS `bfcol_11` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + NOT ( + REGEXP_CONTAINS(`O_COMMENT`, 'special.*requests') + ) +), `bfcte_1` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `bfcol_3`, + COUNT(`bfcol_10`) AS `bfcol_14` + FROM `bfcte_1` + LEFT JOIN `bfcte_0` + ON COALESCE(`bfcol_3`, 0) = COALESCE(`bfcol_11`, 0) + AND COALESCE(`bfcol_3`, 1) = COALESCE(`bfcol_11`, 1) + WHERE + NOT `bfcol_3` IS NULL + GROUP BY + `bfcol_3` +), `bfcte_3` AS ( + SELECT + `bfcol_14`, + COUNT(1) AS `bfcol_16` + FROM `bfcte_2` + WHERE + NOT `bfcol_14` IS NULL + GROUP BY + `bfcol_14` +) +SELECT + `bfcol_14` AS `C_COUNT`, + `bfcol_16` AS `CUSTDIST` +FROM `bfcte_3` +ORDER BY + `bfcol_16` DESC, + `bfcol_14` DESC \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/14/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/14/out.sql new file mode 100644 index 00000000000..a4644a86def --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/14/out.sql @@ -0,0 +1,170 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT('TEMP', 0, 0)]) +), `bfcte_1` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT('TEMP', 0, 0)]) +), `bfcte_2` AS ( + SELECT + `L_PARTKEY` AS `bfcol_0`, + `L_EXTENDEDPRICE` AS `bfcol_1`, + `L_DISCOUNT` AS `bfcol_2`, + `L_SHIPDATE` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `P_PARTKEY` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `P_PARTKEY` AS `bfcol_8`, + `P_TYPE` AS `bfcol_9` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `bfcol_4`, + `bfcol_0`, + `bfcol_1`, + `bfcol_2`, + `bfcol_3`, + `bfcol_1` AS `bfcol_20`, + `bfcol_2` AS `bfcol_21`, + ( + `bfcol_3` >= CAST('1995-09-01' AS DATE) + ) + AND ( + `bfcol_3` < CAST('1995-10-01' AS DATE) + ) AS `bfcol_22`, + `bfcol_1` AS `bfcol_39`, + `bfcol_2` AS `bfcol_40`, + `bfcol_1` AS `bfcol_45`, + 1 - `bfcol_2` AS `bfcol_46`, + `bfcol_1` * ( + 1 - `bfcol_2` + ) AS `bfcol_51` + FROM `bfcte_3` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_4`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_4`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + `bfcol_3` >= CAST('1995-09-01' AS DATE) + ) + AND ( + `bfcol_3` < CAST('1995-10-01' AS DATE) + ) +), `bfcte_6` AS ( + SELECT + `bfcol_8`, + `bfcol_9`, + `bfcol_0`, + `bfcol_1`, + `bfcol_2`, + `bfcol_3`, + `bfcol_9` AS `bfcol_23`, + `bfcol_1` AS `bfcol_24`, + `bfcol_2` AS `bfcol_25`, + ( + `bfcol_3` >= CAST('1995-09-01' AS DATE) + ) + AND ( + `bfcol_3` < CAST('1995-10-01' AS DATE) + ) AS `bfcol_26`, + ( + `bfcol_1` * ( + 1 - `bfcol_2` + ) + ) * CAST(REGEXP_CONTAINS(`bfcol_9`, 'PROMO') AS INT64) AS `bfcol_41` + FROM `bfcte_4` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_8`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_8`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + `bfcol_3` >= CAST('1995-09-01' AS DATE) + ) + AND ( + `bfcol_3` < CAST('1995-10-01' AS DATE) + ) +), `bfcte_7` AS ( + SELECT + COALESCE(SUM(`bfcol_51`), 0) AS `bfcol_54` + FROM `bfcte_5` +), `bfcte_8` AS ( + SELECT + COALESCE(SUM(`bfcol_41`), 0) AS `bfcol_47` + FROM `bfcte_6` +), `bfcte_9` AS ( + SELECT + `bfcol_54`, + 0 AS `bfcol_59` + FROM `bfcte_7` +), `bfcte_10` AS ( + SELECT + `bfcol_47`, + 0 AS `bfcol_50` + FROM `bfcte_8` +), `bfcte_11` AS ( + SELECT + `bfcol_5`, + `bfcol_6`, + `bfcol_7`, + `bfcol_54`, + `bfcol_59`, + CASE WHEN `bfcol_7` = 0 THEN `bfcol_54` END AS `bfcol_64`, + IF(`bfcol_59` = 0, CASE WHEN `bfcol_7` = 0 THEN `bfcol_54` END, NULL) AS `bfcol_72` + FROM `bfcte_0` + CROSS JOIN `bfcte_9` +), `bfcte_12` AS ( + SELECT + `bfcol_10`, + `bfcol_11`, + `bfcol_12`, + `bfcol_47`, + `bfcol_50`, + CASE WHEN `bfcol_12` = 0 THEN `bfcol_47` END AS `bfcol_53`, + IF(`bfcol_50` = 0, CASE WHEN `bfcol_12` = 0 THEN `bfcol_47` END, NULL) AS `bfcol_60` + FROM `bfcte_1` + CROSS JOIN `bfcte_10` +), `bfcte_13` AS ( + SELECT + `bfcol_5`, + `bfcol_6`, + ANY_VALUE(`bfcol_72`) AS `bfcol_79` + FROM `bfcte_11` + WHERE + NOT `bfcol_5` IS NULL AND NOT `bfcol_6` IS NULL + GROUP BY + `bfcol_5`, + `bfcol_6` +), `bfcte_14` AS ( + SELECT + `bfcol_10`, + `bfcol_11`, + ANY_VALUE(`bfcol_60`) AS `bfcol_65` + FROM `bfcte_12` + WHERE + NOT `bfcol_10` IS NULL AND NOT `bfcol_11` IS NULL + GROUP BY + `bfcol_10`, + `bfcol_11` +), `bfcte_15` AS ( + SELECT + `bfcol_5` AS `bfcol_80`, + `bfcol_79` AS `bfcol_81` + FROM `bfcte_13` +), `bfcte_16` AS ( + SELECT + `bfcol_10` AS `bfcol_77`, + 100.0 * `bfcol_65` AS `bfcol_78` + FROM `bfcte_14` +) +SELECT + ROUND(IEEE_DIVIDE(`bfcol_78`, `bfcol_81`), 2) AS `PROMO_REVENUE` +FROM `bfcte_16` +FULL OUTER JOIN `bfcte_15` + ON `bfcol_77` = `bfcol_80` +ORDER BY + COALESCE(`bfcol_77`, `bfcol_80`) ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/15/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/15/out.sql new file mode 100644 index 00000000000..929418a09b2 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/15/out.sql @@ -0,0 +1,116 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT('TOTAL_REVENUE', 0, 0)]) +), `bfcte_1` AS ( + SELECT + `L_SUPPKEY`, + `L_EXTENDEDPRICE`, + `L_DISCOUNT`, + `L_SHIPDATE`, + `L_SUPPKEY` AS `bfcol_12`, + `L_EXTENDEDPRICE` AS `bfcol_13`, + `L_DISCOUNT` AS `bfcol_14`, + ( + `L_SHIPDATE` >= CAST('1996-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1996-04-01' AS DATE) + ) AS `bfcol_15`, + `L_SUPPKEY` AS `bfcol_23`, + `L_EXTENDEDPRICE` * ( + 1 - `L_DISCOUNT` + ) AS `bfcol_24` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `L_SHIPDATE` >= CAST('1996-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1996-04-01' AS DATE) + ) +), `bfcte_2` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_8`, + `S_NAME` AS `bfcol_9`, + `S_ADDRESS` AS `bfcol_10`, + `S_PHONE` AS `bfcol_11` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `bfcol_23`, + COALESCE(SUM(`bfcol_24`), 0) AS `bfcol_27` + FROM `bfcte_1` + WHERE + NOT `bfcol_23` IS NULL + GROUP BY + `bfcol_23` +), `bfcte_5` AS ( + SELECT + `bfcol_23` AS `bfcol_30`, + ROUND(`bfcol_27`, 2) AS `bfcol_31` + FROM `bfcte_4` +), `bfcte_6` AS ( + SELECT + MAX(`bfcol_31`) AS `bfcol_38` + FROM `bfcte_2` + INNER JOIN `bfcte_5` + ON `bfcol_4` = `bfcol_30` +), `bfcte_7` AS ( + SELECT + `bfcol_8` AS `bfcol_33`, + `bfcol_9` AS `bfcol_34`, + `bfcol_10` AS `bfcol_35`, + `bfcol_11` AS `bfcol_36`, + `bfcol_31` AS `bfcol_37` + FROM `bfcte_3` + INNER JOIN `bfcte_5` + ON `bfcol_8` = `bfcol_30` +), `bfcte_8` AS ( + SELECT + `bfcol_38`, + 0 AS `bfcol_39` + FROM `bfcte_6` +), `bfcte_9` AS ( + SELECT + `bfcol_5`, + `bfcol_6`, + `bfcol_7`, + `bfcol_38`, + `bfcol_39`, + CASE WHEN `bfcol_7` = 0 THEN `bfcol_38` END AS `bfcol_40`, + IF(`bfcol_39` = 0, CASE WHEN `bfcol_7` = 0 THEN `bfcol_38` END, NULL) AS `bfcol_45` + FROM `bfcte_0` + CROSS JOIN `bfcte_8` +), `bfcte_10` AS ( + SELECT + `bfcol_5`, + `bfcol_6`, + ANY_VALUE(`bfcol_45`) AS `bfcol_49` + FROM `bfcte_9` + WHERE + NOT `bfcol_5` IS NULL AND NOT `bfcol_6` IS NULL + GROUP BY + `bfcol_5`, + `bfcol_6` +), `bfcte_11` AS ( + SELECT + `bfcol_49` AS `bfcol_50` + FROM `bfcte_10` +) +SELECT + `bfcol_33` AS `S_SUPPKEY`, + `bfcol_34` AS `S_NAME`, + `bfcol_35` AS `S_ADDRESS`, + `bfcol_36` AS `S_PHONE`, + `bfcol_37` AS `TOTAL_REVENUE` +FROM `bfcte_7` +CROSS JOIN `bfcte_11` +WHERE + `bfcol_37` = `bfcol_50` +ORDER BY + `bfcol_33` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/16/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/16/out.sql new file mode 100644 index 00000000000..bd637ec3063 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/16/out.sql @@ -0,0 +1,93 @@ +WITH `bfcte_0` AS ( + SELECT + `S_SUPPKEY`, + `S_COMMENT`, + `S_SUPPKEY` AS `bfcol_8`, + NOT ( + REGEXP_CONTAINS(`S_COMMENT`, 'Customer.*Complaints') + ) AS `bfcol_9` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + NOT ( + REGEXP_CONTAINS(`S_COMMENT`, 'Customer.*Complaints') + ) +), `bfcte_1` AS ( + SELECT + `PS_PARTKEY` AS `bfcol_2`, + `PS_SUPPKEY` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `P_PARTKEY` AS `bfcol_4`, + `P_BRAND` AS `bfcol_5`, + `P_TYPE` AS `bfcol_6`, + `P_SIZE` AS `bfcol_7` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `bfcol_8` + FROM `bfcte_0` + GROUP BY + `bfcol_8` +), `bfcte_4` AS ( + SELECT + `bfcol_5` AS `bfcol_55`, + `bfcol_6` AS `bfcol_56`, + `bfcol_7` AS `bfcol_57`, + `bfcol_3` AS `bfcol_58` + FROM `bfcte_2` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_4`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_4`, 1) = COALESCE(`bfcol_2`, 1) + WHERE + `bfcol_5` <> 'Brand#45' + AND NOT ( + REGEXP_CONTAINS(`bfcol_6`, 'MEDIUM POLISHED') + ) + AND COALESCE(COALESCE(`bfcol_7` IN (49, 14, 23, 45, 19, 3, 36, 9), FALSE), FALSE) +), `bfcte_5` AS ( + SELECT + `bfcol_8` AS `bfcol_21` + FROM `bfcte_3` +), `bfcte_6` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_58`, 0) AS `bfpart1`, COALESCE(`bfcol_58`, 1) AS `bfpart2`) IN ( + ( + SELECT + STRUCT(COALESCE(`bfcol_21`, 0) AS `bfpart1`, COALESCE(`bfcol_21`, 1) AS `bfpart2`) + FROM `bfcte_5` + ) + ) AS `bfcol_59` + FROM `bfcte_4` +), `bfcte_7` AS ( + SELECT + * + FROM `bfcte_6` + WHERE + `bfcol_59` +), `bfcte_8` AS ( + SELECT + `bfcol_55`, + `bfcol_56`, + `bfcol_57`, + COUNT(DISTINCT `bfcol_58`) AS `bfcol_69` + FROM `bfcte_7` + WHERE + NOT `bfcol_55` IS NULL AND NOT `bfcol_56` IS NULL AND NOT `bfcol_57` IS NULL + GROUP BY + `bfcol_55`, + `bfcol_56`, + `bfcol_57` +) +SELECT + `bfcol_55` AS `P_BRAND`, + `bfcol_56` AS `P_TYPE`, + `bfcol_57` AS `P_SIZE`, + `bfcol_69` AS `SUPPLIER_CNT` +FROM `bfcte_8` +ORDER BY + `bfcol_69` DESC, + `bfcol_55` ASC NULLS LAST, + `bfcol_56` ASC NULLS LAST, + `bfcol_57` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/17/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/17/out.sql new file mode 100644 index 00000000000..b9816ff0bf7 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/17/out.sql @@ -0,0 +1,103 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT('L_EXTENDEDPRICE', 0, 0)]) +), `bfcte_1` AS ( + SELECT + `P_PARTKEY` AS `bfcol_15` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `P_BRAND` = 'Brand#23' + ) AND ( + `P_CONTAINER` = 'MED BOX' + ) +), `bfcte_2` AS ( + SELECT + `L_PARTKEY` AS `bfcol_3`, + `L_QUANTITY` AS `bfcol_4`, + `L_EXTENDEDPRICE` AS `bfcol_5` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `L_PARTKEY` AS `bfcol_6`, + `L_QUANTITY` AS `bfcol_7` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `bfcol_4` AS `bfcol_16`, + `bfcol_5` AS `bfcol_17`, + `bfcol_15` AS `bfcol_18` + FROM `bfcte_2` + RIGHT JOIN `bfcte_1` + ON COALESCE(`bfcol_3`, 0) = COALESCE(`bfcol_15`, 0) + AND COALESCE(`bfcol_3`, 1) = COALESCE(`bfcol_15`, 1) +), `bfcte_5` AS ( + SELECT + `bfcol_15`, + AVG(`bfcol_7`) AS `bfcol_21` + FROM `bfcte_3` + RIGHT JOIN `bfcte_1` + ON COALESCE(`bfcol_6`, 0) = COALESCE(`bfcol_15`, 0) + AND COALESCE(`bfcol_6`, 1) = COALESCE(`bfcol_15`, 1) + WHERE + NOT `bfcol_15` IS NULL + GROUP BY + `bfcol_15` +), `bfcte_6` AS ( + SELECT + `bfcol_15` AS `bfcol_24`, + `bfcol_21` * 0.2 AS `bfcol_25` + FROM `bfcte_5` +), `bfcte_7` AS ( + SELECT + `bfcol_24`, + `bfcol_25`, + `bfcol_16`, + `bfcol_17`, + `bfcol_18`, + `bfcol_17` AS `bfcol_29`, + `bfcol_16` < `bfcol_25` AS `bfcol_30` + FROM `bfcte_6` + INNER JOIN `bfcte_4` + ON `bfcol_24` = `bfcol_18` + WHERE + `bfcol_16` < `bfcol_25` +), `bfcte_8` AS ( + SELECT + COALESCE(SUM(`bfcol_29`), 0) AS `bfcol_34` + FROM `bfcte_7` +), `bfcte_9` AS ( + SELECT + `bfcol_34`, + 0 AS `bfcol_35` + FROM `bfcte_8` +), `bfcte_10` AS ( + SELECT + `bfcol_8`, + `bfcol_9`, + `bfcol_10`, + `bfcol_34`, + `bfcol_35`, + CASE WHEN `bfcol_10` = 0 THEN `bfcol_34` END AS `bfcol_36`, + IF(`bfcol_35` = 0, CASE WHEN `bfcol_10` = 0 THEN `bfcol_34` END, NULL) AS `bfcol_41` + FROM `bfcte_0` + CROSS JOIN `bfcte_9` +), `bfcte_11` AS ( + SELECT + `bfcol_8`, + `bfcol_9`, + ANY_VALUE(`bfcol_41`) AS `bfcol_45` + FROM `bfcte_10` + WHERE + NOT `bfcol_8` IS NULL AND NOT `bfcol_9` IS NULL + GROUP BY + `bfcol_8`, + `bfcol_9` +) +SELECT + ROUND(IEEE_DIVIDE(`bfcol_45`, 7.0), 2) AS `AVG_YEARLY` +FROM `bfcte_11` +ORDER BY + `bfcol_9` ASC NULLS LAST, + `bfcol_8` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/18/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/18/out.sql new file mode 100644 index 00000000000..b5720bc932a --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/18/out.sql @@ -0,0 +1,116 @@ +WITH `bfcte_0` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_0`, + `C_NAME` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_2`, + `L_QUANTITY` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_4`, + `O_CUSTKEY` AS `bfcol_5`, + `O_TOTALPRICE` AS `bfcol_6`, + `O_ORDERDATE` AS `bfcol_7` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `bfcol_2`, + COALESCE(SUM(`bfcol_3`), 0) AS `bfcol_8` + FROM `bfcte_1` + WHERE + NOT `bfcol_2` IS NULL + GROUP BY + `bfcol_2` +), `bfcte_4` AS ( + SELECT + `bfcol_2`, + `bfcol_8`, + `bfcol_2` AS `bfcol_9`, + `bfcol_8` > 300 AS `bfcol_10` + FROM `bfcte_3` + WHERE + `bfcol_8` > 300 +), `bfcte_5` AS ( + SELECT + `bfcol_9` + FROM `bfcte_4` + GROUP BY + `bfcol_9` +), `bfcte_6` AS ( + SELECT + `bfcol_9` AS `bfcol_13` + FROM `bfcte_5` +), `bfcte_7` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_4`, 0) AS `bfpart1`, COALESCE(`bfcol_4`, 1) AS `bfpart2`) IN ( + ( + SELECT + STRUCT(COALESCE(`bfcol_13`, 0) AS `bfpart1`, COALESCE(`bfcol_13`, 1) AS `bfpart2`) + FROM `bfcte_6` + ) + ) AS `bfcol_14` + FROM `bfcte_2` +), `bfcte_8` AS ( + SELECT + `bfcol_4` AS `bfcol_20`, + `bfcol_5` AS `bfcol_21`, + `bfcol_6` AS `bfcol_22`, + `bfcol_7` AS `bfcol_23` + FROM `bfcte_7` + WHERE + `bfcol_14` +), `bfcte_9` AS ( + SELECT + `bfcol_20` AS `bfcol_24`, + `bfcol_21` AS `bfcol_25`, + `bfcol_22` AS `bfcol_26`, + `bfcol_23` AS `bfcol_27`, + `bfcol_3` AS `bfcol_28` + FROM `bfcte_8` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_20`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_20`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_1`, + `bfcol_0`, + `bfcol_24`, + `bfcol_27`, + `bfcol_26`, + COALESCE(SUM(`bfcol_28`), 0) AS `bfcol_35` + FROM `bfcte_9` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_25`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_25`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + NOT `bfcol_1` IS NULL + AND NOT `bfcol_0` IS NULL + AND NOT `bfcol_24` IS NULL + AND NOT `bfcol_27` IS NULL + AND NOT `bfcol_26` IS NULL + GROUP BY + `bfcol_1`, + `bfcol_0`, + `bfcol_24`, + `bfcol_27`, + `bfcol_26` +) +SELECT + `bfcol_1` AS `C_NAME`, + `bfcol_0` AS `C_CUSTKEY`, + `bfcol_24` AS `O_ORDERKEY`, + `bfcol_27` AS `O_ORDERDAT`, + `bfcol_26` AS `O_TOTALPRICE`, + `bfcol_35` AS `COL6` +FROM `bfcte_10` +ORDER BY + `bfcol_26` DESC, + `bfcol_27` ASC NULLS LAST, + `bfcol_1` ASC NULLS LAST, + `bfcol_0` ASC NULLS LAST, + `bfcol_24` ASC NULLS LAST +LIMIT 100 \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/19/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/19/out.sql new file mode 100644 index 00000000000..9672739d645 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/19/out.sql @@ -0,0 +1,227 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT(0)]) +), `bfcte_1` AS ( + SELECT + `L_PARTKEY` AS `bfcol_1`, + `L_QUANTITY` AS `bfcol_2`, + `L_EXTENDEDPRICE` AS `bfcol_3`, + `L_DISCOUNT` AS `bfcol_4`, + `L_SHIPINSTRUCT` AS `bfcol_5`, + `L_SHIPMODE` AS `bfcol_6` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `P_PARTKEY` AS `bfcol_7`, + `P_BRAND` AS `bfcol_8`, + `P_SIZE` AS `bfcol_9`, + `P_CONTAINER` AS `bfcol_10` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `bfcol_7`, + `bfcol_8`, + `bfcol_9`, + `bfcol_10`, + `bfcol_1`, + `bfcol_2`, + `bfcol_3`, + `bfcol_4`, + `bfcol_5`, + `bfcol_6`, + `bfcol_3` AS `bfcol_19`, + `bfcol_4` AS `bfcol_20`, + ( + COALESCE(COALESCE(`bfcol_6` IN ('AIR', 'AIR REG'), FALSE), FALSE) + AND ( + `bfcol_5` = 'DELIVER IN PERSON' + ) + ) + AND ( + ( + ( + ( + ( + ( + `bfcol_8` = 'Brand#12' + ) + AND COALESCE(COALESCE(`bfcol_10` IN ('SM CASE', 'SM BOX', 'SM PACK', 'SM PKG'), FALSE), FALSE) + ) + AND ( + ( + `bfcol_2` >= 1 + ) AND ( + `bfcol_2` <= 11 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 5 + ) + ) + ) + OR ( + ( + ( + ( + `bfcol_8` = 'Brand#23' + ) + AND COALESCE( + COALESCE(`bfcol_10` IN ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK'), FALSE), + FALSE + ) + ) + AND ( + ( + `bfcol_2` >= 10 + ) AND ( + `bfcol_2` <= 20 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 10 + ) + ) + ) + ) + OR ( + ( + ( + ( + `bfcol_8` = 'Brand#34' + ) + AND COALESCE(COALESCE(`bfcol_10` IN ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG'), FALSE), FALSE) + ) + AND ( + ( + `bfcol_2` >= 20 + ) AND ( + `bfcol_2` <= 30 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 15 + ) + ) + ) + ) AS `bfcol_21`, + `bfcol_3` AS `bfcol_27`, + 1 - `bfcol_4` AS `bfcol_28`, + `bfcol_3` * ( + 1 - `bfcol_4` + ) AS `bfcol_31` + FROM `bfcte_2` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_7`, 0) = COALESCE(`bfcol_1`, 0) + AND COALESCE(`bfcol_7`, 1) = COALESCE(`bfcol_1`, 1) + WHERE + ( + COALESCE(COALESCE(`bfcol_6` IN ('AIR', 'AIR REG'), FALSE), FALSE) + AND ( + `bfcol_5` = 'DELIVER IN PERSON' + ) + ) + AND ( + ( + ( + ( + ( + ( + `bfcol_8` = 'Brand#12' + ) + AND COALESCE(COALESCE(`bfcol_10` IN ('SM CASE', 'SM BOX', 'SM PACK', 'SM PKG'), FALSE), FALSE) + ) + AND ( + ( + `bfcol_2` >= 1 + ) AND ( + `bfcol_2` <= 11 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 5 + ) + ) + ) + OR ( + ( + ( + ( + `bfcol_8` = 'Brand#23' + ) + AND COALESCE( + COALESCE(`bfcol_10` IN ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK'), FALSE), + FALSE + ) + ) + AND ( + ( + `bfcol_2` >= 10 + ) AND ( + `bfcol_2` <= 20 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 10 + ) + ) + ) + ) + OR ( + ( + ( + ( + `bfcol_8` = 'Brand#34' + ) + AND COALESCE(COALESCE(`bfcol_10` IN ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG'), FALSE), FALSE) + ) + AND ( + ( + `bfcol_2` >= 20 + ) AND ( + `bfcol_2` <= 30 + ) + ) + ) + AND ( + ( + `bfcol_9` >= 1 + ) AND ( + `bfcol_9` <= 15 + ) + ) + ) + ) +), `bfcte_4` AS ( + SELECT + COALESCE(SUM(`bfcol_31`), 0) AS `bfcol_33` + FROM `bfcte_3` +), `bfcte_5` AS ( + SELECT + * + FROM `bfcte_4` +) +SELECT + CASE WHEN `bfcol_0` = 0 THEN `bfcol_33` END AS `REVENUE` +FROM `bfcte_5` +CROSS JOIN `bfcte_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/2/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/2/out.sql new file mode 100644 index 00000000000..9130dc95fce --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/2/out.sql @@ -0,0 +1,210 @@ +WITH `bfcte_0` AS ( + SELECT + `R_REGIONKEY` AS `bfcol_0`, + `R_NAME` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`REGION` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_2`, + `N_NAME` AS `bfcol_3`, + `N_REGIONKEY` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_19`, + `N_REGIONKEY` AS `bfcol_20` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_5`, + `S_NAME` AS `bfcol_6`, + `S_ADDRESS` AS `bfcol_7`, + `S_NATIONKEY` AS `bfcol_8`, + `S_PHONE` AS `bfcol_9`, + `S_ACCTBAL` AS `bfcol_10`, + `S_COMMENT` AS `bfcol_11` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_21`, + `S_NATIONKEY` AS `bfcol_22` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `PS_PARTKEY` AS `bfcol_12`, + `PS_SUPPKEY` AS `bfcol_13`, + `PS_SUPPLYCOST` AS `bfcol_14` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_6` AS ( + SELECT + `P_PARTKEY` AS `bfcol_15`, + `P_MFGR` AS `bfcol_16`, + `P_TYPE` AS `bfcol_17`, + `P_SIZE` AS `bfcol_18` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_7` AS ( + SELECT + `P_PARTKEY` AS `bfcol_23`, + `P_TYPE` AS `bfcol_24`, + `P_SIZE` AS `bfcol_25` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_8` AS ( + SELECT + `bfcol_15` AS `bfcol_26`, + `bfcol_16` AS `bfcol_27`, + `bfcol_17` AS `bfcol_28`, + `bfcol_18` AS `bfcol_29`, + `bfcol_13` AS `bfcol_30`, + `bfcol_14` AS `bfcol_31` + FROM `bfcte_6` + INNER JOIN `bfcte_5` + ON COALESCE(`bfcol_15`, 0) = COALESCE(`bfcol_12`, 0) + AND COALESCE(`bfcol_15`, 1) = COALESCE(`bfcol_12`, 1) +), `bfcte_9` AS ( + SELECT + `bfcol_23` AS `bfcol_32`, + `bfcol_24` AS `bfcol_33`, + `bfcol_25` AS `bfcol_34`, + `bfcol_13` AS `bfcol_35`, + `bfcol_14` AS `bfcol_36` + FROM `bfcte_7` + INNER JOIN `bfcte_5` + ON COALESCE(`bfcol_23`, 0) = COALESCE(`bfcol_12`, 0) + AND COALESCE(`bfcol_23`, 1) = COALESCE(`bfcol_12`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_26` AS `bfcol_37`, + `bfcol_27` AS `bfcol_38`, + `bfcol_28` AS `bfcol_39`, + `bfcol_29` AS `bfcol_40`, + `bfcol_31` AS `bfcol_41`, + `bfcol_6` AS `bfcol_42`, + `bfcol_7` AS `bfcol_43`, + `bfcol_8` AS `bfcol_44`, + `bfcol_9` AS `bfcol_45`, + `bfcol_10` AS `bfcol_46`, + `bfcol_11` AS `bfcol_47` + FROM `bfcte_8` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_30`, 0) = COALESCE(`bfcol_5`, 0) + AND COALESCE(`bfcol_30`, 1) = COALESCE(`bfcol_5`, 1) +), `bfcte_11` AS ( + SELECT + `bfcol_32` AS `bfcol_48`, + `bfcol_33` AS `bfcol_49`, + `bfcol_34` AS `bfcol_50`, + `bfcol_36` AS `bfcol_51`, + `bfcol_22` AS `bfcol_52` + FROM `bfcte_9` + INNER JOIN `bfcte_4` + ON COALESCE(`bfcol_35`, 0) = COALESCE(`bfcol_21`, 0) + AND COALESCE(`bfcol_35`, 1) = COALESCE(`bfcol_21`, 1) +), `bfcte_12` AS ( + SELECT + `bfcol_37` AS `bfcol_53`, + `bfcol_38` AS `bfcol_54`, + `bfcol_39` AS `bfcol_55`, + `bfcol_40` AS `bfcol_56`, + `bfcol_41` AS `bfcol_57`, + `bfcol_42` AS `bfcol_58`, + `bfcol_43` AS `bfcol_59`, + `bfcol_45` AS `bfcol_60`, + `bfcol_46` AS `bfcol_61`, + `bfcol_47` AS `bfcol_62`, + `bfcol_3` AS `bfcol_63`, + `bfcol_4` AS `bfcol_64` + FROM `bfcte_10` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_44`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_44`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_13` AS ( + SELECT + `bfcol_48` AS `bfcol_65`, + `bfcol_49` AS `bfcol_66`, + `bfcol_50` AS `bfcol_67`, + `bfcol_51` AS `bfcol_68`, + `bfcol_20` AS `bfcol_69` + FROM `bfcte_11` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_52`, 0) = COALESCE(`bfcol_19`, 0) + AND COALESCE(`bfcol_52`, 1) = COALESCE(`bfcol_19`, 1) +), `bfcte_14` AS ( + SELECT + `bfcol_53` AS `bfcol_205`, + `bfcol_54` AS `bfcol_206`, + `bfcol_57` AS `bfcol_207`, + `bfcol_58` AS `bfcol_208`, + `bfcol_59` AS `bfcol_209`, + `bfcol_60` AS `bfcol_210`, + `bfcol_61` AS `bfcol_211`, + `bfcol_62` AS `bfcol_212`, + `bfcol_63` AS `bfcol_213` + FROM `bfcte_12` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_64`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_64`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + `bfcol_56` = 15 AND ENDS_WITH(`bfcol_55`, 'BRASS') AND `bfcol_1` = 'EUROPE' +), `bfcte_15` AS ( + SELECT + `bfcol_65`, + `bfcol_66`, + `bfcol_67`, + `bfcol_68`, + `bfcol_69`, + `bfcol_0`, + `bfcol_1`, + `bfcol_65` AS `bfcol_99`, + `bfcol_66` AS `bfcol_100`, + `bfcol_68` AS `bfcol_101`, + `bfcol_1` AS `bfcol_102`, + `bfcol_67` = 15 AS `bfcol_103`, + `bfcol_65` AS `bfcol_147`, + `bfcol_68` AS `bfcol_148`, + `bfcol_1` AS `bfcol_149`, + ENDS_WITH(`bfcol_66`, 'BRASS') AS `bfcol_150`, + `bfcol_65` AS `bfcol_189`, + `bfcol_68` AS `bfcol_190`, + `bfcol_1` = 'EUROPE' AS `bfcol_191` + FROM `bfcte_13` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_69`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_69`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + `bfcol_67` = 15 AND ENDS_WITH(`bfcol_66`, 'BRASS') AND `bfcol_1` = 'EUROPE' +), `bfcte_16` AS ( + SELECT + `bfcol_189`, + MIN(`bfcol_190`) AS `bfcol_216` + FROM `bfcte_15` + WHERE + NOT `bfcol_189` IS NULL + GROUP BY + `bfcol_189` +), `bfcte_17` AS ( + SELECT + `bfcol_189` AS `bfcol_214`, + `bfcol_216` + FROM `bfcte_16` +) +SELECT + `bfcol_211` AS `S_ACCTBAL`, + `bfcol_208` AS `S_NAME`, + `bfcol_213` AS `N_NAME`, + `bfcol_214` AS `P_PARTKEY`, + `bfcol_206` AS `P_MFGR`, + `bfcol_209` AS `S_ADDRESS`, + `bfcol_210` AS `S_PHONE`, + `bfcol_212` AS `S_COMMENT` +FROM `bfcte_17` +INNER JOIN `bfcte_14` + ON COALESCE(`bfcol_214`, 0) = COALESCE(`bfcol_205`, 0) + AND COALESCE(`bfcol_214`, 1) = COALESCE(`bfcol_205`, 1) + AND IF(IS_NAN(`bfcol_216`), 2.0, COALESCE(`bfcol_216`, 0.0)) = IF(IS_NAN(`bfcol_207`), 2.0, COALESCE(`bfcol_207`, 0.0)) + AND IF(IS_NAN(`bfcol_216`), 3, COALESCE(`bfcol_216`, 1.0)) = IF(IS_NAN(`bfcol_207`), 3, COALESCE(`bfcol_207`, 1.0)) +ORDER BY + `bfcol_211` DESC, + `bfcol_213` ASC NULLS LAST, + `bfcol_208` ASC NULLS LAST, + `bfcol_214` ASC NULLS LAST +LIMIT 100 \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/20/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/20/out.sql new file mode 100644 index 00000000000..8c9cd9bb763 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/20/out.sql @@ -0,0 +1,151 @@ +WITH `bfcte_0` AS ( + SELECT + `P_PARTKEY`, + `P_NAME`, + `P_PARTKEY` AS `bfcol_15`, + STARTS_WITH(`P_NAME`, 'forest') AS `bfcol_16` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + STARTS_WITH(`P_NAME`, 'forest') +), `bfcte_1` AS ( + SELECT + `PS_PARTKEY` AS `bfcol_2`, + `PS_SUPPKEY` AS `bfcol_3`, + `PS_AVAILQTY` AS `bfcol_4` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `L_PARTKEY`, + `L_SUPPKEY`, + `L_QUANTITY`, + `L_SHIPDATE`, + `L_PARTKEY` AS `bfcol_17`, + `L_SUPPKEY` AS `bfcol_18`, + `L_QUANTITY` AS `bfcol_19`, + ( + `L_SHIPDATE` >= CAST('1994-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1995-01-01' AS DATE) + ) AS `bfcol_20` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `L_SHIPDATE` >= CAST('1994-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1995-01-01' AS DATE) + ) +), `bfcte_3` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_35` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `N_NAME` = 'CANADA' +), `bfcte_4` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_11`, + `S_NAME` AS `bfcol_12`, + `S_ADDRESS` AS `bfcol_13`, + `S_NATIONKEY` AS `bfcol_14` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `bfcol_15` + FROM `bfcte_0` + GROUP BY + `bfcol_15` +), `bfcte_6` AS ( + SELECT + `bfcol_17`, + `bfcol_18`, + COALESCE(SUM(`bfcol_19`), 0) AS `bfcol_36` + FROM `bfcte_2` + WHERE + NOT `bfcol_17` IS NULL AND NOT `bfcol_18` IS NULL + GROUP BY + `bfcol_17`, + `bfcol_18` +), `bfcte_7` AS ( + SELECT + `bfcol_11` AS `bfcol_41`, + `bfcol_12` AS `bfcol_42`, + `bfcol_13` AS `bfcol_43` + FROM `bfcte_4` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_14`, 0) = COALESCE(`bfcol_35`, 0) + AND COALESCE(`bfcol_14`, 1) = COALESCE(`bfcol_35`, 1) +), `bfcte_8` AS ( + SELECT + `bfcol_15` AS `bfcol_31` + FROM `bfcte_5` +), `bfcte_9` AS ( + SELECT + `bfcol_17` AS `bfcol_48`, + `bfcol_18` AS `bfcol_49`, + `bfcol_36` * 0.5 AS `bfcol_50` + FROM `bfcte_6` +), `bfcte_10` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_2`, 0) AS `bfpart1`, COALESCE(`bfcol_2`, 1) AS `bfpart2`) IN ( + ( + SELECT + STRUCT(COALESCE(`bfcol_31`, 0) AS `bfpart1`, COALESCE(`bfcol_31`, 1) AS `bfpart2`) + FROM `bfcte_8` + ) + ) AS `bfcol_37` + FROM `bfcte_1` +), `bfcte_11` AS ( + SELECT + `bfcol_2` AS `bfcol_51`, + `bfcol_3` AS `bfcol_52`, + `bfcol_4` AS `bfcol_53` + FROM `bfcte_10` + WHERE + `bfcol_37` +), `bfcte_12` AS ( + SELECT + `bfcol_48`, + `bfcol_49`, + `bfcol_50`, + `bfcol_51`, + `bfcol_52`, + `bfcol_53`, + `bfcol_52` AS `bfcol_57`, + `bfcol_53` > `bfcol_50` AS `bfcol_58` + FROM `bfcte_9` + INNER JOIN `bfcte_11` + ON `bfcol_49` = `bfcol_52` AND `bfcol_48` = `bfcol_51` + WHERE + `bfcol_53` > `bfcol_50` +), `bfcte_13` AS ( + SELECT + `bfcol_57` + FROM `bfcte_12` + GROUP BY + `bfcol_57` +), `bfcte_14` AS ( + SELECT + `bfcol_57` AS `bfcol_61` + FROM `bfcte_13` +), `bfcte_15` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_41`, 0) AS `bfpart1`, COALESCE(`bfcol_41`, 1) AS `bfpart2`) IN ( + ( + SELECT + STRUCT(COALESCE(`bfcol_61`, 0) AS `bfpart1`, COALESCE(`bfcol_61`, 1) AS `bfpart2`) + FROM `bfcte_14` + ) + ) AS `bfcol_62` + FROM `bfcte_7` +) +SELECT + `bfcol_42` AS `S_NAME`, + `bfcol_43` AS `S_ADDRESS` +FROM `bfcte_15` +WHERE + `bfcol_62` +ORDER BY + `bfcol_42` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/21/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/21/out.sql new file mode 100644 index 00000000000..93a44e529d9 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/21/out.sql @@ -0,0 +1,148 @@ +WITH `bfcte_0` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_0`, + `O_ORDERSTATUS` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_2`, + `N_NAME` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_4`, + `S_NAME` AS `bfcol_5`, + `S_NATIONKEY` AS `bfcol_6` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_30`, + `L_SUPPKEY` AS `bfcol_31` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `L_RECEIPTDATE` > `L_COMMITDATE` +), `bfcte_4` AS ( + SELECT + `L_ORDERKEY` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_32` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `L_RECEIPTDATE` > `L_COMMITDATE` +), `bfcte_6` AS ( + SELECT + `L_ORDERKEY`, + COUNT(1) AS `bfcol_18` + FROM `bfcte_4` + WHERE + NOT `L_ORDERKEY` IS NULL + GROUP BY + `L_ORDERKEY` +), `bfcte_7` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_33` + FROM `bfcte_6` + WHERE + `bfcol_18` > 1 +), `bfcte_8` AS ( + SELECT + `bfcol_33` AS `bfcol_34`, + `bfcol_31` AS `bfcol_35` + FROM `bfcte_7` + INNER JOIN `bfcte_3` + ON `bfcol_33` = `bfcol_30` +), `bfcte_9` AS ( + SELECT + `bfcol_33`, + COUNT(1) AS `bfcol_37` + FROM `bfcte_7` + INNER JOIN `bfcte_5` + ON `bfcol_33` = `bfcol_32` + GROUP BY + `bfcol_33` +), `bfcte_10` AS ( + SELECT + `bfcol_33` AS `bfcol_36`, + `bfcol_37` + FROM `bfcte_9` +), `bfcte_11` AS ( + SELECT + `bfcol_36` AS `bfcol_38`, + `bfcol_37` AS `bfcol_39`, + `bfcol_35` AS `bfcol_40` + FROM `bfcte_10` + INNER JOIN `bfcte_8` + ON `bfcol_36` = `bfcol_34` +), `bfcte_12` AS ( + SELECT + `bfcol_38` AS `bfcol_41`, + `bfcol_39` AS `bfcol_42`, + `bfcol_5` AS `bfcol_43`, + `bfcol_6` AS `bfcol_44` + FROM `bfcte_11` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_40`, 0) = COALESCE(`bfcol_4`, 0) + AND COALESCE(`bfcol_40`, 1) = COALESCE(`bfcol_4`, 1) +), `bfcte_13` AS ( + SELECT + `bfcol_41` AS `bfcol_45`, + `bfcol_42` AS `bfcol_46`, + `bfcol_43` AS `bfcol_47`, + `bfcol_3` AS `bfcol_48` + FROM `bfcte_12` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_44`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_44`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_14` AS ( + SELECT + `bfcol_45`, + `bfcol_46`, + `bfcol_47`, + `bfcol_48`, + `bfcol_0`, + `bfcol_1`, + `bfcol_47` AS `bfcol_53`, + ( + ( + `bfcol_46` = 1 + ) AND ( + `bfcol_48` = 'SAUDI ARABIA' + ) + ) + AND ( + `bfcol_1` = 'F' + ) AS `bfcol_54` + FROM `bfcte_13` + INNER JOIN `bfcte_0` + ON `bfcol_45` = `bfcol_0` + WHERE + ( + ( + `bfcol_46` = 1 + ) AND ( + `bfcol_48` = 'SAUDI ARABIA' + ) + ) + AND ( + `bfcol_1` = 'F' + ) +), `bfcte_15` AS ( + SELECT + `bfcol_53`, + COUNT(1) AS `bfcol_58` + FROM `bfcte_14` + WHERE + NOT `bfcol_53` IS NULL + GROUP BY + `bfcol_53` +) +SELECT + `bfcol_53` AS `S_NAME`, + `bfcol_58` AS `NUMWAIT` +FROM `bfcte_15` +ORDER BY + `bfcol_58` DESC, + `bfcol_53` ASC NULLS LAST +LIMIT 100 \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/22/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/22/out.sql new file mode 100644 index 00000000000..87ca2d8d5e0 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/22/out.sql @@ -0,0 +1,136 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT('C_ACCTBAL', 0, 0)]) +), `bfcte_1` AS ( + SELECT + `O_CUSTKEY` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `C_PHONE`, + `C_ACCTBAL`, + `C_ACCTBAL` AS `bfcol_9`, + SUBSTRING(`C_PHONE`, 1, 2) AS `bfcol_10`, + `C_ACCTBAL` AS `bfcol_19`, + COALESCE( + COALESCE(SUBSTRING(`C_PHONE`, 1, 2) IN ('13', '31', '23', '29', '30', '18', '17'), FALSE), + FALSE + ) AS `bfcol_20`, + `C_ACCTBAL` AS `bfcol_35`, + `C_ACCTBAL` > 0.0 AS `bfcol_36` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + COALESCE( + COALESCE(SUBSTRING(`C_PHONE`, 1, 2) IN ('13', '31', '23', '29', '30', '18', '17'), FALSE), + FALSE + ) + AND `C_ACCTBAL` > 0.0 +), `bfcte_3` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_32`, + `C_ACCTBAL` AS `bfcol_33`, + SUBSTRING(`C_PHONE`, 1, 2) AS `bfcol_34` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + COALESCE( + COALESCE(SUBSTRING(`C_PHONE`, 1, 2) IN ('13', '31', '23', '29', '30', '18', '17'), FALSE), + FALSE + ) +), `bfcte_4` AS ( + SELECT + `O_CUSTKEY` + FROM `bfcte_1` + GROUP BY + `O_CUSTKEY` +), `bfcte_5` AS ( + SELECT + AVG(`bfcol_35`) AS `bfcol_40` + FROM `bfcte_2` +), `bfcte_6` AS ( + SELECT + `O_CUSTKEY` AS `bfcol_0` + FROM `bfcte_4` +), `bfcte_7` AS ( + SELECT + `bfcol_40`, + 0 AS `bfcol_41` + FROM `bfcte_5` +), `bfcte_8` AS ( + SELECT + `bfcol_3`, + `bfcol_4`, + `bfcol_5`, + `bfcol_40`, + `bfcol_41`, + CASE WHEN `bfcol_5` = 0 THEN `bfcol_40` END AS `bfcol_42`, + IF(`bfcol_41` = 0, CASE WHEN `bfcol_5` = 0 THEN `bfcol_40` END, NULL) AS `bfcol_47` + FROM `bfcte_0` + CROSS JOIN `bfcte_7` +), `bfcte_9` AS ( + SELECT + `bfcol_3`, + `bfcol_4`, + ANY_VALUE(`bfcol_47`) AS `bfcol_51` + FROM `bfcte_8` + WHERE + NOT `bfcol_3` IS NULL AND NOT `bfcol_4` IS NULL + GROUP BY + `bfcol_3`, + `bfcol_4` +), `bfcte_10` AS ( + SELECT + `bfcol_51` AS `bfcol_52` + FROM `bfcte_9` +), `bfcte_11` AS ( + SELECT + `bfcol_32` AS `bfcol_61`, + `bfcol_33` AS `bfcol_62`, + `bfcol_34` AS `bfcol_63` + FROM `bfcte_3` + CROSS JOIN `bfcte_10` + WHERE + `bfcol_33` > `bfcol_52` +), `bfcte_12` AS ( + SELECT + *, + STRUCT(COALESCE(`bfcol_61`, 0) AS `bfpart1`, COALESCE(`bfcol_61`, 1) AS `bfpart2`) IN ( + ( + SELECT + STRUCT(COALESCE(`bfcol_0`, 0) AS `bfpart1`, COALESCE(`bfcol_0`, 1) AS `bfpart2`) + FROM `bfcte_6` + ) + ) AS `bfcol_64` + FROM `bfcte_11` +), `bfcte_13` AS ( + SELECT + `bfcol_61`, + `bfcol_62`, + `bfcol_63`, + `bfcol_64`, + NOT ( + `bfcol_64` + ) AS `bfcol_65` + FROM `bfcte_12` + WHERE + NOT ( + `bfcol_64` + ) +), `bfcte_14` AS ( + SELECT + `bfcol_63`, + COUNT(`bfcol_61`) AS `bfcol_73`, + COALESCE(SUM(`bfcol_62`), 0) AS `bfcol_74` + FROM `bfcte_13` + WHERE + NOT `bfcol_63` IS NULL + GROUP BY + `bfcol_63` +) +SELECT + `bfcol_63` AS `CNTRYCODE`, + `bfcol_73` AS `NUMCUST`, + `bfcol_74` AS `TOTACCTBAL` +FROM `bfcte_14` +ORDER BY + `bfcol_63` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/3/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/3/out.sql new file mode 100644 index 00000000000..0d1365d76d1 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/3/out.sql @@ -0,0 +1,80 @@ +WITH `bfcte_0` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_32`, + `O_CUSTKEY` AS `bfcol_33`, + `O_ORDERDATE` AS `bfcol_34`, + `O_SHIPPRIORITY` AS `bfcol_35` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `O_ORDERDATE` < CAST('1995-03-15' AS DATE) +), `bfcte_1` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_36`, + `L_EXTENDEDPRICE` AS `bfcol_37`, + `L_DISCOUNT` AS `bfcol_38` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `L_SHIPDATE` > CAST('1995-03-15' AS DATE) +), `bfcte_2` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_39` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `C_MKTSEGMENT` = 'BUILDING' +), `bfcte_3` AS ( + SELECT + `bfcol_37` AS `bfcol_40`, + `bfcol_38` AS `bfcol_41`, + `bfcol_32` AS `bfcol_42`, + `bfcol_33` AS `bfcol_43`, + `bfcol_34` AS `bfcol_44`, + `bfcol_35` AS `bfcol_45` + FROM `bfcte_1` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_36`, 0) = COALESCE(`bfcol_32`, 0) + AND COALESCE(`bfcol_36`, 1) = COALESCE(`bfcol_32`, 1) +), `bfcte_4` AS ( + SELECT + `bfcol_39`, + `bfcol_40`, + `bfcol_41`, + `bfcol_42`, + `bfcol_43`, + `bfcol_44`, + `bfcol_45`, + `bfcol_42` AS `bfcol_51`, + `bfcol_44` AS `bfcol_52`, + `bfcol_45` AS `bfcol_53`, + `bfcol_40` * ( + 1 - `bfcol_41` + ) AS `bfcol_54` + FROM `bfcte_2` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_39`, 0) = COALESCE(`bfcol_43`, 0) + AND COALESCE(`bfcol_39`, 1) = COALESCE(`bfcol_43`, 1) +), `bfcte_5` AS ( + SELECT + `bfcol_51`, + `bfcol_52`, + `bfcol_53`, + COALESCE(SUM(`bfcol_54`), 0) AS `bfcol_59` + FROM `bfcte_4` + WHERE + NOT `bfcol_51` IS NULL AND NOT `bfcol_52` IS NULL AND NOT `bfcol_53` IS NULL + GROUP BY + `bfcol_51`, + `bfcol_52`, + `bfcol_53` +) +SELECT + `bfcol_51` AS `L_ORDERKEY`, + `bfcol_59` AS `REVENUE`, + `bfcol_52` AS `O_ORDERDATE`, + `bfcol_53` AS `O_SHIPPRIORITY` +FROM `bfcte_5` +ORDER BY + `bfcol_59` DESC, + `bfcol_52` ASC NULLS LAST, + `bfcol_51` ASC NULLS LAST, + `bfcol_53` ASC NULLS LAST +LIMIT 10 \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/4/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/4/out.sql new file mode 100644 index 00000000000..9eb0259be50 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/4/out.sql @@ -0,0 +1,70 @@ +WITH `bfcte_0` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_0`, + `O_ORDERDATE` AS `bfcol_1`, + `O_ORDERPRIORITY` AS `bfcol_2` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_3`, + `L_COMMITDATE` AS `bfcol_4`, + `L_RECEIPTDATE` AS `bfcol_5` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `bfcol_3`, + `bfcol_4`, + `bfcol_5`, + `bfcol_0`, + `bfcol_1`, + `bfcol_2`, + `bfcol_3` AS `bfcol_11`, + `bfcol_4` AS `bfcol_12`, + `bfcol_5` AS `bfcol_13`, + `bfcol_2` AS `bfcol_14`, + ( + `bfcol_1` >= CAST('1993-07-01' AS DATE) + ) + AND ( + `bfcol_1` < CAST('1993-10-01' AS DATE) + ) AS `bfcol_15`, + `bfcol_3` AS `bfcol_25`, + `bfcol_2` AS `bfcol_26`, + `bfcol_4` < `bfcol_5` AS `bfcol_27` + FROM `bfcte_1` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_3`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_3`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + `bfcol_1` >= CAST('1993-07-01' AS DATE) + ) + AND ( + `bfcol_1` < CAST('1993-10-01' AS DATE) + ) + AND `bfcol_4` < `bfcol_5` +), `bfcte_3` AS ( + SELECT + `bfcol_26`, + `bfcol_25`, + COUNT(1) AS `bfcol_33` + FROM `bfcte_2` + WHERE + NOT `bfcol_26` IS NULL AND NOT `bfcol_25` IS NULL + GROUP BY + `bfcol_26`, + `bfcol_25` +), `bfcte_4` AS ( + SELECT + `bfcol_26`, + COUNT(`bfcol_25`) AS `bfcol_36` + FROM `bfcte_3` + GROUP BY + `bfcol_26` +) +SELECT + `bfcol_26` AS `O_ORDERPRIORITY`, + `bfcol_36` AS `ORDER_COUNT` +FROM `bfcte_4` +ORDER BY + `bfcol_26` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/5/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/5/out.sql new file mode 100644 index 00000000000..34974b36d8f --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/5/out.sql @@ -0,0 +1,100 @@ +WITH `bfcte_0` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_0`, + `S_NATIONKEY` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_5` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_2`, + `C_NATIONKEY` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_4`, + `N_NAME` AS `bfcol_5`, + `N_REGIONKEY` AS `bfcol_6` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `R_REGIONKEY` AS `bfcol_32` + FROM `bigframes-dev`.`tpch`.`REGION` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + `R_NAME` = 'ASIA' +), `bfcte_4` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_33`, + `O_CUSTKEY` AS `bfcol_34` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `O_ORDERDATE` >= CAST('1994-01-01' AS DATE) + ) + AND ( + `O_ORDERDATE` < CAST('1995-01-01' AS DATE) + ) +), `bfcte_5` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_29`, + `L_SUPPKEY` AS `bfcol_30`, + `L_EXTENDEDPRICE` * ( + 1.0 - `L_DISCOUNT` + ) AS `bfcol_31` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_6` AS ( + SELECT + `bfcol_4` AS `bfcol_35`, + `bfcol_5` AS `bfcol_36` + FROM `bfcte_3` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_32`, 0) = COALESCE(`bfcol_6`, 0) + AND COALESCE(`bfcol_32`, 1) = COALESCE(`bfcol_6`, 1) +), `bfcte_7` AS ( + SELECT + `bfcol_35` AS `bfcol_37`, + `bfcol_36` AS `bfcol_38`, + `bfcol_2` AS `bfcol_39` + FROM `bfcte_6` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_35`, 0) = COALESCE(`bfcol_3`, 0) + AND COALESCE(`bfcol_35`, 1) = COALESCE(`bfcol_3`, 1) +), `bfcte_8` AS ( + SELECT + `bfcol_33` AS `bfcol_40`, + `bfcol_37` AS `bfcol_41`, + `bfcol_38` AS `bfcol_42` + FROM `bfcte_4` + INNER JOIN `bfcte_7` + ON COALESCE(`bfcol_34`, 0) = COALESCE(`bfcol_39`, 0) + AND COALESCE(`bfcol_34`, 1) = COALESCE(`bfcol_39`, 1) +), `bfcte_9` AS ( + SELECT + `bfcol_30` AS `bfcol_43`, + `bfcol_31` AS `bfcol_44`, + `bfcol_41` AS `bfcol_45`, + `bfcol_42` AS `bfcol_46` + FROM `bfcte_5` + INNER JOIN `bfcte_8` + ON COALESCE(`bfcol_29`, 0) = COALESCE(`bfcol_40`, 0) + AND COALESCE(`bfcol_29`, 1) = COALESCE(`bfcol_40`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_46`, + COALESCE(SUM(`bfcol_44`), 0) AS `bfcol_49` + FROM `bfcte_9` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_43`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_43`, 1) = COALESCE(`bfcol_0`, 1) + AND COALESCE(`bfcol_45`, 0) = COALESCE(`bfcol_1`, 0) + AND COALESCE(`bfcol_45`, 1) = COALESCE(`bfcol_1`, 1) + WHERE + NOT `bfcol_46` IS NULL + GROUP BY + `bfcol_46` +) +SELECT + `bfcol_46` AS `N_NAME`, + `bfcol_49` AS `REVENUE` +FROM `bfcte_10` +ORDER BY + `bfcol_49` DESC, + `bfcol_46` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/6/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/6/out.sql new file mode 100644 index 00000000000..110f3f9736f --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/6/out.sql @@ -0,0 +1,61 @@ +WITH `bfcte_0` AS ( + SELECT + * + FROM UNNEST(ARRAY>[STRUCT(0)]) +), `bfcte_1` AS ( + SELECT + `L_QUANTITY`, + `L_EXTENDEDPRICE`, + `L_DISCOUNT`, + `L_SHIPDATE`, + `L_QUANTITY` AS `bfcol_5`, + `L_EXTENDEDPRICE` AS `bfcol_6`, + `L_DISCOUNT` AS `bfcol_7`, + ( + `L_SHIPDATE` >= CAST('1994-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1995-01-01' AS DATE) + ) AS `bfcol_8`, + `L_QUANTITY` AS `bfcol_16`, + `L_EXTENDEDPRICE` AS `bfcol_17`, + `L_DISCOUNT` AS `bfcol_18`, + ( + `L_DISCOUNT` >= 0.05 + ) AND ( + `L_DISCOUNT` <= 0.07 + ) AS `bfcol_19`, + `L_EXTENDEDPRICE` AS `bfcol_27`, + `L_DISCOUNT` AS `bfcol_28`, + `L_QUANTITY` < 24 AS `bfcol_29`, + `L_EXTENDEDPRICE` AS `bfcol_35`, + `L_DISCOUNT` AS `bfcol_36`, + `L_EXTENDEDPRICE` * `L_DISCOUNT` AS `bfcol_39` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `L_SHIPDATE` >= CAST('1994-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` < CAST('1995-01-01' AS DATE) + ) + AND ( + `L_DISCOUNT` >= 0.05 + ) + AND ( + `L_DISCOUNT` <= 0.07 + ) + AND `L_QUANTITY` < 24 +), `bfcte_2` AS ( + SELECT + COALESCE(SUM(`bfcol_39`), 0) AS `bfcol_41` + FROM `bfcte_1` +), `bfcte_3` AS ( + SELECT + * + FROM `bfcte_2` +) +SELECT + CASE WHEN `bfcol_0` = 0 THEN `bfcol_41` END AS `REVENUE` +FROM `bfcte_3` +CROSS JOIN `bfcte_0` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/7/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/7/out.sql new file mode 100644 index 00000000000..3d82a905e8b --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/7/out.sql @@ -0,0 +1,143 @@ +WITH `bfcte_0` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_22`, + `N_NAME` AS `bfcol_23`, + COALESCE(COALESCE(`N_NAME` IN ('FRANCE', 'GERMANY'), FALSE), FALSE) AS `bfcol_24` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + COALESCE(COALESCE(`N_NAME` IN ('FRANCE', 'GERMANY'), FALSE), FALSE) +), `bfcte_1` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_2`, + `S_NATIONKEY` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_31`, + `L_SUPPKEY` AS `bfcol_32`, + `L_EXTENDEDPRICE` AS `bfcol_33`, + `L_DISCOUNT` AS `bfcol_34`, + `L_SHIPDATE` AS `bfcol_35` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' + WHERE + ( + `L_SHIPDATE` >= CAST('1995-01-01' AS DATE) + ) + AND ( + `L_SHIPDATE` <= CAST('1996-12-31' AS DATE) + ) +), `bfcte_3` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_9`, + `O_CUSTKEY` AS `bfcol_10` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_11`, + `C_NATIONKEY` AS `bfcol_12` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `bfcol_22` AS `bfcol_36`, + `bfcol_23` AS `bfcol_37` + FROM `bfcte_0` +), `bfcte_6` AS ( + SELECT + `bfcol_22` AS `bfcol_38`, + `bfcol_23` AS `bfcol_39` + FROM `bfcte_0` +), `bfcte_7` AS ( + SELECT + `bfcol_11` AS `bfcol_40`, + `bfcol_39` AS `bfcol_41` + FROM `bfcte_4` + INNER JOIN `bfcte_6` + ON COALESCE(`bfcol_12`, 0) = COALESCE(`bfcol_38`, 0) + AND COALESCE(`bfcol_12`, 1) = COALESCE(`bfcol_38`, 1) +), `bfcte_8` AS ( + SELECT + `bfcol_41` AS `bfcol_42`, + `bfcol_9` AS `bfcol_43` + FROM `bfcte_7` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_40`, 0) = COALESCE(`bfcol_10`, 0) + AND COALESCE(`bfcol_40`, 1) = COALESCE(`bfcol_10`, 1) +), `bfcte_9` AS ( + SELECT + `bfcol_42` AS `bfcol_44`, + `bfcol_32` AS `bfcol_45`, + `bfcol_33` AS `bfcol_46`, + `bfcol_34` AS `bfcol_47`, + `bfcol_35` AS `bfcol_48` + FROM `bfcte_8` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_43`, 0) = COALESCE(`bfcol_31`, 0) + AND COALESCE(`bfcol_43`, 1) = COALESCE(`bfcol_31`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_44` AS `bfcol_49`, + `bfcol_46` AS `bfcol_50`, + `bfcol_47` AS `bfcol_51`, + `bfcol_48` AS `bfcol_52`, + `bfcol_3` AS `bfcol_53` + FROM `bfcte_9` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_45`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_45`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_11` AS ( + SELECT + `bfcol_49`, + `bfcol_50`, + `bfcol_51`, + `bfcol_52`, + `bfcol_53`, + `bfcol_36`, + `bfcol_37`, + `bfcol_49` AS `bfcol_59`, + `bfcol_50` AS `bfcol_60`, + `bfcol_51` AS `bfcol_61`, + `bfcol_52` AS `bfcol_62`, + `bfcol_37` AS `bfcol_63`, + `bfcol_49` <> `bfcol_37` AS `bfcol_64`, + `bfcol_49` AS `bfcol_76`, + `bfcol_52` AS `bfcol_77`, + `bfcol_37` AS `bfcol_78`, + `bfcol_50` * ( + 1.0 - `bfcol_51` + ) AS `bfcol_79`, + `bfcol_49` AS `bfcol_84`, + `bfcol_37` AS `bfcol_85`, + `bfcol_50` * ( + 1.0 - `bfcol_51` + ) AS `bfcol_86`, + EXTRACT(YEAR FROM `bfcol_52`) AS `bfcol_87` + FROM `bfcte_10` + INNER JOIN `bfcte_5` + ON COALESCE(`bfcol_53`, 0) = COALESCE(`bfcol_36`, 0) + AND COALESCE(`bfcol_53`, 1) = COALESCE(`bfcol_36`, 1) + WHERE + `bfcol_49` <> `bfcol_37` +), `bfcte_12` AS ( + SELECT + `bfcol_85`, + `bfcol_84`, + `bfcol_87`, + COALESCE(SUM(`bfcol_86`), 0) AS `bfcol_92` + FROM `bfcte_11` + WHERE + NOT `bfcol_85` IS NULL AND NOT `bfcol_84` IS NULL AND NOT `bfcol_87` IS NULL + GROUP BY + `bfcol_85`, + `bfcol_84`, + `bfcol_87` +) +SELECT + `bfcol_85` AS `SUPP_NATION`, + `bfcol_84` AS `CUST_NATION`, + `bfcol_87` AS `L_YEAR`, + `bfcol_92` AS `REVENUE` +FROM `bfcte_12` +ORDER BY + `bfcol_85` ASC NULLS LAST, + `bfcol_84` ASC NULLS LAST, + `bfcol_87` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/8/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/8/out.sql new file mode 100644 index 00000000000..b2fa2971caf --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/8/out.sql @@ -0,0 +1,193 @@ +WITH `bfcte_0` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_0`, + `N_NAME` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_6` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_4`, + `N_REGIONKEY` AS `bfcol_5` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_6` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `R_REGIONKEY` AS `bfcol_2`, + `R_NAME` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`REGION` AS `bft_5` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `C_CUSTKEY` AS `bfcol_6`, + `C_NATIONKEY` AS `bfcol_7` + FROM `bigframes-dev`.`tpch`.`CUSTOMER` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_8`, + `O_CUSTKEY` AS `bfcol_9`, + `O_ORDERDATE` AS `bfcol_10` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_11`, + `S_NATIONKEY` AS `bfcol_12` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_6` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_13`, + `L_PARTKEY` AS `bfcol_14`, + `L_SUPPKEY` AS `bfcol_15`, + `L_EXTENDEDPRICE` AS `bfcol_16`, + `L_DISCOUNT` AS `bfcol_17` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_7` AS ( + SELECT + `P_PARTKEY` AS `bfcol_18`, + `P_TYPE` AS `bfcol_19` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_8` AS ( + SELECT + `bfcol_19` AS `bfcol_20`, + `bfcol_13` AS `bfcol_21`, + `bfcol_15` AS `bfcol_22`, + `bfcol_16` AS `bfcol_23`, + `bfcol_17` AS `bfcol_24` + FROM `bfcte_7` + INNER JOIN `bfcte_6` + ON COALESCE(`bfcol_18`, 0) = COALESCE(`bfcol_14`, 0) + AND COALESCE(`bfcol_18`, 1) = COALESCE(`bfcol_14`, 1) +), `bfcte_9` AS ( + SELECT + `bfcol_20` AS `bfcol_25`, + `bfcol_21` AS `bfcol_26`, + `bfcol_23` AS `bfcol_27`, + `bfcol_24` AS `bfcol_28`, + `bfcol_12` AS `bfcol_29` + FROM `bfcte_8` + INNER JOIN `bfcte_5` + ON COALESCE(`bfcol_22`, 0) = COALESCE(`bfcol_11`, 0) + AND COALESCE(`bfcol_22`, 1) = COALESCE(`bfcol_11`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_25` AS `bfcol_30`, + `bfcol_27` AS `bfcol_31`, + `bfcol_28` AS `bfcol_32`, + `bfcol_29` AS `bfcol_33`, + `bfcol_9` AS `bfcol_34`, + `bfcol_10` AS `bfcol_35` + FROM `bfcte_9` + INNER JOIN `bfcte_4` + ON COALESCE(`bfcol_26`, 0) = COALESCE(`bfcol_8`, 0) + AND COALESCE(`bfcol_26`, 1) = COALESCE(`bfcol_8`, 1) +), `bfcte_11` AS ( + SELECT + `bfcol_30` AS `bfcol_36`, + `bfcol_31` AS `bfcol_37`, + `bfcol_32` AS `bfcol_38`, + `bfcol_33` AS `bfcol_39`, + `bfcol_35` AS `bfcol_40`, + `bfcol_7` AS `bfcol_41` + FROM `bfcte_10` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_34`, 0) = COALESCE(`bfcol_6`, 0) + AND COALESCE(`bfcol_34`, 1) = COALESCE(`bfcol_6`, 1) +), `bfcte_12` AS ( + SELECT + `bfcol_36` AS `bfcol_42`, + `bfcol_37` AS `bfcol_43`, + `bfcol_38` AS `bfcol_44`, + `bfcol_39` AS `bfcol_45`, + `bfcol_40` AS `bfcol_46`, + `bfcol_5` AS `bfcol_47` + FROM `bfcte_11` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_41`, 0) = COALESCE(`bfcol_4`, 0) + AND COALESCE(`bfcol_41`, 1) = COALESCE(`bfcol_4`, 1) +), `bfcte_13` AS ( + SELECT + `bfcol_42` AS `bfcol_66`, + `bfcol_43` AS `bfcol_67`, + `bfcol_44` AS `bfcol_68`, + `bfcol_45` AS `bfcol_69`, + `bfcol_46` AS `bfcol_70` + FROM `bfcte_12` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_47`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_47`, 1) = COALESCE(`bfcol_2`, 1) + WHERE + `bfcol_3` = 'AMERICA' +), `bfcte_14` AS ( + SELECT + `bfcol_66`, + `bfcol_67`, + `bfcol_68`, + `bfcol_69`, + `bfcol_70`, + `bfcol_0`, + `bfcol_1`, + `bfcol_66` AS `bfcol_76`, + `bfcol_67` AS `bfcol_77`, + `bfcol_68` AS `bfcol_78`, + `bfcol_70` AS `bfcol_79`, + `bfcol_1` AS `bfcol_80`, + ( + `bfcol_70` >= CAST('1995-01-01' AS DATE) + ) + AND ( + `bfcol_70` <= CAST('1996-12-31' AS DATE) + ) AS `bfcol_81`, + `bfcol_67` AS `bfcol_93`, + `bfcol_68` AS `bfcol_94`, + `bfcol_70` AS `bfcol_95`, + `bfcol_1` AS `bfcol_96`, + `bfcol_66` = 'ECONOMY ANODIZED STEEL' AS `bfcol_97`, + `bfcol_67` AS `bfcol_107`, + `bfcol_68` AS `bfcol_108`, + `bfcol_1` AS `bfcol_109`, + EXTRACT(YEAR FROM `bfcol_70`) AS `bfcol_110`, + `bfcol_1` AS `bfcol_115`, + EXTRACT(YEAR FROM `bfcol_70`) AS `bfcol_116`, + `bfcol_67` * ( + 1.0 - `bfcol_68` + ) AS `bfcol_117`, + EXTRACT(YEAR FROM `bfcol_70`) AS `bfcol_121`, + `bfcol_67` * ( + 1.0 - `bfcol_68` + ) AS `bfcol_122`, + IF(`bfcol_1` = 'BRAZIL', `bfcol_67` * ( + 1.0 - `bfcol_68` + ), 0) AS `bfcol_123`, + EXTRACT(YEAR FROM `bfcol_70`) AS `bfcol_127`, + IF(`bfcol_1` = 'BRAZIL', `bfcol_67` * ( + 1.0 - `bfcol_68` + ), 0) AS `bfcol_128`, + `bfcol_67` * ( + 1.0 - `bfcol_68` + ) AS `bfcol_129` + FROM `bfcte_13` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_69`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_69`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + ( + `bfcol_70` >= CAST('1995-01-01' AS DATE) + ) + AND ( + `bfcol_70` <= CAST('1996-12-31' AS DATE) + ) + AND `bfcol_66` = 'ECONOMY ANODIZED STEEL' +), `bfcte_15` AS ( + SELECT + `bfcol_127`, + COALESCE(SUM(`bfcol_128`), 0) AS `bfcol_133`, + COALESCE(SUM(`bfcol_129`), 0) AS `bfcol_134` + FROM `bfcte_14` + WHERE + NOT `bfcol_127` IS NULL + GROUP BY + `bfcol_127` +) +SELECT + `bfcol_127` AS `O_YEAR`, + ROUND(IEEE_DIVIDE(`bfcol_133`, `bfcol_134`), 2) AS `MKT_SHARE` +FROM `bfcte_15` +ORDER BY + `bfcol_127` ASC NULLS LAST, + `bfcol_127` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/9/out.sql b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/9/out.sql new file mode 100644 index 00000000000..7f886aa7ce5 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/snapshots/test_tpch/test_tpch_query/9/out.sql @@ -0,0 +1,150 @@ +WITH `bfcte_0` AS ( + SELECT + `N_NATIONKEY` AS `bfcol_0`, + `N_NAME` AS `bfcol_1` + FROM `bigframes-dev`.`tpch`.`NATION` AS `bft_5` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_1` AS ( + SELECT + `O_ORDERKEY` AS `bfcol_2`, + `O_ORDERDATE` AS `bfcol_3` + FROM `bigframes-dev`.`tpch`.`ORDERS` AS `bft_4` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_2` AS ( + SELECT + `S_SUPPKEY` AS `bfcol_4`, + `S_NATIONKEY` AS `bfcol_5` + FROM `bigframes-dev`.`tpch`.`SUPPLIER` AS `bft_3` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_3` AS ( + SELECT + `PS_PARTKEY` AS `bfcol_6`, + `PS_SUPPKEY` AS `bfcol_7`, + `PS_SUPPLYCOST` AS `bfcol_8` + FROM `bigframes-dev`.`tpch`.`PARTSUPP` AS `bft_2` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_4` AS ( + SELECT + `L_ORDERKEY` AS `bfcol_9`, + `L_PARTKEY` AS `bfcol_10`, + `L_SUPPKEY` AS `bfcol_11`, + `L_QUANTITY` AS `bfcol_12`, + `L_EXTENDEDPRICE` AS `bfcol_13`, + `L_DISCOUNT` AS `bfcol_14` + FROM `bigframes-dev`.`tpch`.`LINEITEM` AS `bft_1` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_5` AS ( + SELECT + `P_PARTKEY` AS `bfcol_15`, + `P_NAME` AS `bfcol_16` + FROM `bigframes-dev`.`tpch`.`PART` AS `bft_0` FOR SYSTEM_TIME AS OF '2026-03-10T18:00:00' +), `bfcte_6` AS ( + SELECT + `bfcol_16` AS `bfcol_17`, + `bfcol_9` AS `bfcol_18`, + `bfcol_10` AS `bfcol_19`, + `bfcol_11` AS `bfcol_20`, + `bfcol_12` AS `bfcol_21`, + `bfcol_13` AS `bfcol_22`, + `bfcol_14` AS `bfcol_23` + FROM `bfcte_5` + INNER JOIN `bfcte_4` + ON COALESCE(`bfcol_15`, 0) = COALESCE(`bfcol_10`, 0) + AND COALESCE(`bfcol_15`, 1) = COALESCE(`bfcol_10`, 1) +), `bfcte_7` AS ( + SELECT + `bfcol_17` AS `bfcol_24`, + `bfcol_18` AS `bfcol_25`, + `bfcol_20` AS `bfcol_26`, + `bfcol_21` AS `bfcol_27`, + `bfcol_22` AS `bfcol_28`, + `bfcol_23` AS `bfcol_29`, + `bfcol_8` AS `bfcol_30` + FROM `bfcte_6` + INNER JOIN `bfcte_3` + ON COALESCE(`bfcol_20`, 0) = COALESCE(`bfcol_7`, 0) + AND COALESCE(`bfcol_20`, 1) = COALESCE(`bfcol_7`, 1) + AND COALESCE(`bfcol_19`, 0) = COALESCE(`bfcol_6`, 0) + AND COALESCE(`bfcol_19`, 1) = COALESCE(`bfcol_6`, 1) +), `bfcte_8` AS ( + SELECT + `bfcol_24` AS `bfcol_31`, + `bfcol_25` AS `bfcol_32`, + `bfcol_27` AS `bfcol_33`, + `bfcol_28` AS `bfcol_34`, + `bfcol_29` AS `bfcol_35`, + `bfcol_30` AS `bfcol_36`, + `bfcol_5` AS `bfcol_37` + FROM `bfcte_7` + INNER JOIN `bfcte_2` + ON COALESCE(`bfcol_26`, 0) = COALESCE(`bfcol_4`, 0) + AND COALESCE(`bfcol_26`, 1) = COALESCE(`bfcol_4`, 1) +), `bfcte_9` AS ( + SELECT + `bfcol_31` AS `bfcol_38`, + `bfcol_33` AS `bfcol_39`, + `bfcol_34` AS `bfcol_40`, + `bfcol_35` AS `bfcol_41`, + `bfcol_36` AS `bfcol_42`, + `bfcol_37` AS `bfcol_43`, + `bfcol_3` AS `bfcol_44` + FROM `bfcte_8` + INNER JOIN `bfcte_1` + ON COALESCE(`bfcol_32`, 0) = COALESCE(`bfcol_2`, 0) + AND COALESCE(`bfcol_32`, 1) = COALESCE(`bfcol_2`, 1) +), `bfcte_10` AS ( + SELECT + `bfcol_38`, + `bfcol_39`, + `bfcol_40`, + `bfcol_41`, + `bfcol_42`, + `bfcol_43`, + `bfcol_44`, + `bfcol_0`, + `bfcol_1`, + `bfcol_39` AS `bfcol_52`, + `bfcol_40` AS `bfcol_53`, + `bfcol_41` AS `bfcol_54`, + `bfcol_42` AS `bfcol_55`, + `bfcol_44` AS `bfcol_56`, + `bfcol_1` AS `bfcol_57`, + REGEXP_CONTAINS(`bfcol_38`, 'green') AS `bfcol_58`, + `bfcol_39` AS `bfcol_72`, + `bfcol_40` AS `bfcol_73`, + `bfcol_41` AS `bfcol_74`, + `bfcol_42` AS `bfcol_75`, + `bfcol_1` AS `bfcol_76`, + EXTRACT(YEAR FROM `bfcol_44`) AS `bfcol_77`, + `bfcol_1` AS `bfcol_84`, + EXTRACT(YEAR FROM `bfcol_44`) AS `bfcol_85`, + ( + `bfcol_40` * ( + 1 - `bfcol_41` + ) + ) - ( + `bfcol_42` * `bfcol_39` + ) AS `bfcol_86` + FROM `bfcte_9` + INNER JOIN `bfcte_0` + ON COALESCE(`bfcol_43`, 0) = COALESCE(`bfcol_0`, 0) + AND COALESCE(`bfcol_43`, 1) = COALESCE(`bfcol_0`, 1) + WHERE + REGEXP_CONTAINS(`bfcol_38`, 'green') +), `bfcte_11` AS ( + SELECT + `bfcol_84`, + `bfcol_85`, + COALESCE(SUM(`bfcol_86`), 0) AS `bfcol_90` + FROM `bfcte_10` + WHERE + NOT `bfcol_84` IS NULL AND NOT `bfcol_85` IS NULL + GROUP BY + `bfcol_84`, + `bfcol_85` +) +SELECT + `bfcol_84` AS `NATION`, + `bfcol_85` AS `O_YEAR`, + ROUND(`bfcol_90`, 2) AS `SUM_PROFIT` +FROM `bfcte_11` +ORDER BY + `bfcol_84` ASC NULLS LAST, + `bfcol_85` DESC, + `bfcol_84` ASC NULLS LAST, + `bfcol_85` ASC NULLS LAST \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/tpch/test_tpch.py b/tests/unit/core/compile/sqlglot/tpch/test_tpch.py new file mode 100644 index 00000000000..042d8d55d40 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/tpch/test_tpch.py @@ -0,0 +1,50 @@ +# Copyright 2026 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 re + +import pytest + +freezegun = pytest.importorskip("freezegun") +pytest.importorskip("pytest_snapshot") + + +@pytest.mark.parametrize("query_num", range(1, 23)) +def test_tpch_query(tpch_session, query_num, snapshot): + project_id = "bigframes-dev" + dataset_id = "tpch" + + query_file_path = f"third_party/bigframes_vendored/tpch/queries/q{query_num}.py" + + with open(query_file_path, "r") as f: + query_code = f.read() + + # We want to capture the result dataframe instead of running next(result.to_pandas_batches(...)) + modified_code = re.sub( + r"next\((\w+)\.to_pandas_batches\((.*?)\)\)", + r"return \1", + query_code, + ) + + exec_globals = {} # type: ignore[var-annotated] + exec(modified_code, exec_globals) + q_func = exec_globals["q"] + + with freezegun.freeze_time("2026-03-10 18:00:00"): + result = q_func(project_id, dataset_id, tpch_session) + + # result should be a DataFrame + sql = result.sql + + snapshot.assert_match(sql, "out.sql") diff --git a/tests/unit/core/sql/snapshots/test_ml/test_evaluate_model_with_options/evaluate_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_evaluate_model_with_options/evaluate_model_with_options.sql index 848c36907b9..cdb66bbf0e1 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_evaluate_model_with_options/evaluate_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_evaluate_model_with_options/evaluate_model_with_options.sql @@ -1 +1 @@ -SELECT * FROM ML.EVALUATE(MODEL `my_model`, STRUCT(false AS perform_aggregation, 10 AS horizon, 0.95 AS confidence_level)) +SELECT * FROM ML.EVALUATE(MODEL `my_model`, STRUCT(FALSE AS `perform_aggregation`, 10 AS `horizon`, 0.95 AS `confidence_level`)) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_explain_predict_model_with_options/explain_predict_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_explain_predict_model_with_options/explain_predict_model_with_options.sql index 1214bba8706..7569463ea2d 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_explain_predict_model_with_options/explain_predict_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_explain_predict_model_with_options/explain_predict_model_with_options.sql @@ -1 +1 @@ -SELECT * FROM ML.EXPLAIN_PREDICT(MODEL `my_model`, (SELECT * FROM new_data), STRUCT(5 AS top_k_features)) +SELECT * FROM ML.EXPLAIN_PREDICT(MODEL `my_model`, (SELECT * FROM new_data), STRUCT(5 AS `top_k_features`)) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_generate_embedding_model_with_options/generate_embedding_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_generate_embedding_model_with_options/generate_embedding_model_with_options.sql index d07e1c1e15e..3be957079cf 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_generate_embedding_model_with_options/generate_embedding_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_generate_embedding_model_with_options/generate_embedding_model_with_options.sql @@ -1 +1,5 @@ -SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project.my_dataset.my_model`, (SELECT * FROM new_data), STRUCT(true AS flatten_json_output, 'RETRIEVAL_DOCUMENT' AS task_type, 256 AS output_dimensionality)) +SELECT * FROM ML.GENERATE_EMBEDDING(MODEL `my_project.my_dataset.my_model`, (SELECT * FROM new_data), STRUCT( + TRUE AS `flatten_json_output`, + 'RETRIEVAL_DOCUMENT' AS `task_type`, + 256 AS `output_dimensionality` +)) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_generate_text_model_with_options/generate_text_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_generate_text_model_with_options/generate_text_model_with_options.sql index 7839ff3fbdd..0ea26747287 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_generate_text_model_with_options/generate_text_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_generate_text_model_with_options/generate_text_model_with_options.sql @@ -1 +1,10 @@ -SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project.my_dataset.my_model`, (SELECT * FROM new_data), STRUCT(0.5 AS temperature, 128 AS max_output_tokens, 20 AS top_k, 0.9 AS top_p, true AS flatten_json_output, ['a', 'b'] AS stop_sequences, true AS ground_with_google_search, 'TYPE' AS request_type)) +SELECT * FROM ML.GENERATE_TEXT(MODEL `my_project.my_dataset.my_model`, (SELECT * FROM new_data), STRUCT( + 0.5 AS `temperature`, + 128 AS `max_output_tokens`, + 20 AS `top_k`, + 0.9 AS `top_p`, + TRUE AS `flatten_json_output`, + ['a', 'b'] AS `stop_sequences`, + TRUE AS `ground_with_google_search`, + 'TYPE' AS `request_type` +)) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_get_insights_model_basic/get_insights_model_basic.sql b/tests/unit/core/sql/snapshots/test_ml/test_get_insights_model_basic/get_insights_model_basic.sql new file mode 100644 index 00000000000..a3f2680c179 --- /dev/null +++ b/tests/unit/core/sql/snapshots/test_ml/test_get_insights_model_basic/get_insights_model_basic.sql @@ -0,0 +1 @@ +SELECT * FROM ML.GET_INSIGHTS(MODEL `my_project.my_dataset.my_model`) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_global_explain_model_with_options/global_explain_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_global_explain_model_with_options/global_explain_model_with_options.sql index b8d158acfc7..396648aa1db 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_global_explain_model_with_options/global_explain_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_global_explain_model_with_options/global_explain_model_with_options.sql @@ -1 +1 @@ -SELECT * FROM ML.GLOBAL_EXPLAIN(MODEL `my_model`, STRUCT(true AS class_level_explain)) +SELECT * FROM ML.GLOBAL_EXPLAIN(MODEL `my_model`, STRUCT(TRUE AS `class_level_explain`)) diff --git a/tests/unit/core/sql/snapshots/test_ml/test_predict_model_with_options/predict_model_with_options.sql b/tests/unit/core/sql/snapshots/test_ml/test_predict_model_with_options/predict_model_with_options.sql index f320d47fcf4..e19f39eebba 100644 --- a/tests/unit/core/sql/snapshots/test_ml/test_predict_model_with_options/predict_model_with_options.sql +++ b/tests/unit/core/sql/snapshots/test_ml/test_predict_model_with_options/predict_model_with_options.sql @@ -1 +1 @@ -SELECT * FROM ML.PREDICT(MODEL `my_model`, (SELECT * FROM new_data), STRUCT(true AS keep_original_columns)) +SELECT * FROM ML.PREDICT(MODEL `my_model`, (SELECT * FROM new_data), STRUCT(TRUE AS `keep_original_columns`)) diff --git a/tests/unit/core/sql/test_io.py b/tests/unit/core/sql/test_io.py deleted file mode 100644 index 23e5f796e31..00000000000 --- a/tests/unit/core/sql/test_io.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2026 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 -# 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 bigframes.core.sql.io - - -def test_load_data_ddl(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" - assert sql == expected - - -def test_load_data_ddl_overwrite(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - write_disposition="OVERWRITE", - columns={"col1": "INT64", "col2": "STRING"}, - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA OVERWRITE my-project.my_dataset.my_table (col1 INT64, col2 STRING) FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" - assert sql == expected - - -def test_load_data_ddl_with_partition_columns(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - with_partition_columns={"part1": "DATE", "part2": "STRING"}, - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) FROM FILES (format = 'CSV', uris = ['gs://bucket/path*']) WITH PARTITION COLUMNS (part1 DATE, part2 STRING)" - assert sql == expected - - -def test_load_data_ddl_connection(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - connection_name="my-connection", - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) FROM FILES (format = 'CSV', uris = ['gs://bucket/path*']) WITH CONNECTION `my-connection`" - assert sql == expected - - -def test_load_data_ddl_partition_by(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - partition_by=["date_col"], - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) PARTITION BY date_col FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" - assert sql == expected - - -def test_load_data_ddl_cluster_by(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - cluster_by=["cluster_col"], - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) CLUSTER BY cluster_col FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" - assert sql == expected - - -def test_load_data_ddl_table_options(): - sql = bigframes.core.sql.io.load_data_ddl( - "my-project.my_dataset.my_table", - columns={"col1": "INT64", "col2": "STRING"}, - table_options={"description": "my table"}, - from_files_options={"format": "CSV", "uris": ["gs://bucket/path*"]}, - ) - expected = "LOAD DATA INTO my-project.my_dataset.my_table (col1 INT64, col2 STRING) OPTIONS (description = 'my table') FROM FILES (format = 'CSV', uris = ['gs://bucket/path*'])" - assert sql == expected diff --git a/tests/unit/core/sql/test_ml.py b/tests/unit/core/sql/test_ml.py index 27b7a00ac21..bb3b61a949c 100644 --- a/tests/unit/core/sql/test_ml.py +++ b/tests/unit/core/sql/test_ml.py @@ -203,6 +203,13 @@ def test_generate_text_model_with_options(snapshot): snapshot.assert_match(sql, "generate_text_model_with_options.sql") +def test_get_insights_model_basic(snapshot): + sql = bigframes.core.sql.ml.get_insights( + model_name="my_project.my_dataset.my_model", + ) + snapshot.assert_match(sql, "get_insights_model_basic.sql") + + def test_generate_embedding_model_basic(snapshot): sql = bigframes.core.sql.ml.generate_embedding( model_name="my_project.my_dataset.my_model", diff --git a/tests/unit/core/test_groupby.py b/tests/unit/core/test_groupby.py index faee007c3d8..b23199da331 100644 --- a/tests/unit/core/test_groupby.py +++ b/tests/unit/core/test_groupby.py @@ -18,7 +18,7 @@ import bigframes.core.utils as utils import bigframes.pandas as bpd -import bigframes.testing +import bigframes.testing.utils pytest.importorskip("polars") pytest.importorskip("pandas", minversion="2.0.0") @@ -33,7 +33,7 @@ def test_groupby_df_iter_by_key_singular(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -47,7 +47,7 @@ def test_groupby_df_iter_by_key_list(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -69,7 +69,7 @@ def test_groupby_df_iter_by_key_list_multiple(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -85,7 +85,7 @@ def test_groupby_df_iter_by_level_singular(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -109,7 +109,7 @@ def test_groupby_df_iter_by_level_list_one_item(polars_session): assert bf_key == tuple(pd_key) else: assert bf_key == (pd_key,) - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -131,7 +131,7 @@ def test_groupby_df_iter_by_level_list_multiple(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_frame_equal( + bigframes.testing.utils.assert_frame_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -149,7 +149,7 @@ def test_groupby_series_iter_by_level_singular(polars_session): bf_result = bf_group_series.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -175,7 +175,7 @@ def test_groupby_series_iter_by_level_list_one_item(polars_session): assert bf_key == tuple(pd_key) else: assert bf_key == (pd_key,) - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -199,7 +199,7 @@ def test_groupby_series_iter_by_level_list_multiple(polars_session): bf_result = bf_group_df.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -218,7 +218,7 @@ def test_groupby_series_iter_by_series(polars_session): bf_result = bf_group_series.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -237,7 +237,7 @@ def test_groupby_series_iter_by_series_list_one_item(polars_session): bf_result = bf_group_series.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) @@ -259,6 +259,6 @@ def test_groupby_series_iter_by_series_list_multiple(polars_session): bf_result = bf_group_series.to_pandas() pd_key, pd_result = pd_group assert bf_key == pd_key - bigframes.testing.assert_series_equal( + bigframes.testing.utils.assert_series_equal( bf_result, pd_result, check_dtype=False, check_index_type=False ) diff --git a/tests/unit/core/test_sql.py b/tests/unit/core/test_sql.py index 17da3008fc4..04ebb28764d 100644 --- a/tests/unit/core/test_sql.py +++ b/tests/unit/core/test_sql.py @@ -12,128 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime -import decimal -import re - -import pytest -import shapely.geometry # type: ignore - from bigframes.core import sql -@pytest.mark.parametrize( - ("value", "expected_pattern"), - ( - # Try to have some literals for each scalar data type: - # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types - (None, "NULL"), - # TODO: support ARRAY type (possibly another method?) - (True, "True"), - (False, "False"), - ( - b"\x01\x02\x03ABC", - re.escape(r"b'\x01\x02\x03ABC'"), - ), - ( - datetime.date(2025, 1, 1), - re.escape("DATE('2025-01-01')"), - ), - ( - datetime.datetime(2025, 1, 2, 3, 45, 6, 789123), - re.escape("DATETIME('2025-01-02T03:45:06.789123')"), - ), - ( - shapely.geometry.Point(0, 1), - r"ST_GEOGFROMTEXT\('POINT \(0[.]?0* 1[.]?0*\)'\)", - ), - # TODO: INTERVAL type (e.g. from dateutil.relativedelta) - # TODO: JSON type (TBD what Python object that would correspond to) - (123, re.escape("123")), - (decimal.Decimal("123.75"), re.escape("CAST('123.75' AS NUMERIC)")), - # TODO: support BIGNUMERIC by looking at precision/scale of the DECIMAL - (123.75, re.escape("123.75")), - # TODO: support RANGE type - ("abc", re.escape("'abc'")), - # TODO: support STRUCT type (possibly another method?) - ( - datetime.time(12, 34, 56, 789123), - re.escape("TIME(DATETIME('1970-01-01 12:34:56.789123'))"), - ), - ( - datetime.datetime( - 2025, 1, 2, 3, 45, 6, 789123, tzinfo=datetime.timezone.utc - ), - re.escape("TIMESTAMP('2025-01-02T03:45:06.789123+00:00')"), - ), - ), -) -def test_simple_literal(value, expected_pattern): - got = sql.simple_literal(value) - assert re.match(expected_pattern, got) is not None - - -@pytest.mark.parametrize( - ("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], re.escape("[NULL, NULL]")), - ([True, False], re.escape("[True, False]")), - ( - [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)], - re.escape("[DATE('2025-01-01'), DATE('2025-01-01')]"), - ), - ( - [datetime.datetime(2025, 1, 2, 3, 45, 6, 789123)], - re.escape("[DATETIME('2025-01-02T03:45:06.789123')]"), - ), - ( - [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], re.escape("[123, 456]")), - ( - [decimal.Decimal("123.75"), decimal.Decimal("456.78")], - 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], re.escape("[123.75, 456.78]")), - # TODO: support RANGE type - (["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)], - re.escape( - "[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 - ), - ], - 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_pattern: str): - got = sql.simple_literal(value) - assert re.match(expected_pattern, got) is not None - - 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", @@ -180,6 +61,6 @@ def test_create_vector_search_sql_all_named_parameters(): query_column_to_search => 'another_embedding_column', top_k=> 10, distance_type => 'cosine', -options => '{\\"fraction_lists_to_search\\": 0.1, \\"use_brute_force\\": false}') +options => '{"fraction_lists_to_search": 0.1, "use_brute_force": false}') """ ) diff --git a/tests/unit/extensions/__init__.py b/tests/unit/extensions/__init__.py new file mode 100644 index 00000000000..58d482ea386 --- /dev/null +++ b/tests/unit/extensions/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 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. diff --git a/tests/unit/extensions/pandas/__init__.py b/tests/unit/extensions/pandas/__init__.py new file mode 100644 index 00000000000..58d482ea386 --- /dev/null +++ b/tests/unit/extensions/pandas/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 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. diff --git a/tests/unit/extensions/pandas/test_registration.py b/tests/unit/extensions/pandas/test_registration.py new file mode 100644 index 00000000000..12580980916 --- /dev/null +++ b/tests/unit/extensions/pandas/test_registration.py @@ -0,0 +1,27 @@ +# Copyright 2026 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 + +# Importing bigframes registers the accessor. +import bigframes # noqa: F401 + + +def test_bigframes_import_registers_accessor(): + df = pd.DataFrame({"a": [1]}) + # If bigframes was imported, df.bigquery should exist + assert hasattr(df, "bigquery") + from bigframes.extensions.pandas.dataframe_accessor import BigQueryDataFrameAccessor + + assert isinstance(df.bigquery, BigQueryDataFrameAccessor) diff --git a/tests/unit/ml/test_golden_sql.py b/tests/unit/ml/test_golden_sql.py index 7f6843aacf6..d3d880f87ae 100644 --- a/tests/unit/ml/test_golden_sql.py +++ b/tests/unit/ml/test_golden_sql.py @@ -124,7 +124,7 @@ def test_linear_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=True,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=TRUE,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=FALSE,\n enable_global_explain=FALSE,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" ) @@ -134,7 +134,7 @@ def test_linear_regression_params_fit(bqml_model_factory, mock_session, mock_X, model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=False,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LINEAR_REG',\n data_split_method='NO_SPLIT',\n optimize_strategy='auto_strategy',\n fit_intercept=FALSE,\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=FALSE,\n enable_global_explain=FALSE,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" ) @@ -169,7 +169,7 @@ def test_logistic_regression_default_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=True,\n auto_class_weights=False,\n optimize_strategy='auto_strategy',\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=False,\n enable_global_explain=False,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql", + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=TRUE,\n auto_class_weights=FALSE,\n optimize_strategy='auto_strategy',\n l2_reg=0.0,\n max_iterations=20,\n learn_rate_strategy='line_search',\n min_rel_progress=0.01,\n calculate_p_values=FALSE,\n enable_global_explain=FALSE,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql", ) @@ -191,7 +191,7 @@ def test_logistic_regression_params_fit( model.fit(mock_X, mock_y) mock_session._start_query_ml_ddl.assert_called_once_with( - "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=False,\n auto_class_weights=True,\n optimize_strategy='batch_gradient_descent',\n l2_reg=0.2,\n max_iterations=30,\n learn_rate_strategy='constant',\n min_rel_progress=0.02,\n calculate_p_values=False,\n enable_global_explain=False,\n l1_reg=0.2,\n learn_rate=0.2,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" + "CREATE OR REPLACE MODEL `test-project`.`_anon123`.`temp_model_id`\nOPTIONS(\n model_type='LOGISTIC_REG',\n data_split_method='NO_SPLIT',\n fit_intercept=FALSE,\n auto_class_weights=TRUE,\n optimize_strategy='batch_gradient_descent',\n l2_reg=0.2,\n max_iterations=30,\n learn_rate_strategy='constant',\n min_rel_progress=0.02,\n calculate_p_values=FALSE,\n enable_global_explain=FALSE,\n l1_reg=0.2,\n learn_rate=0.2,\n INPUT_LABEL_COLS=['input_column_label'])\nAS input_X_y_no_index_sql" ) diff --git a/tests/unit/session/test_io_bigquery.py b/tests/unit/session/test_io_bigquery.py index eb58c6bb52d..3d3832d6786 100644 --- a/tests/unit/session/test_io_bigquery.py +++ b/tests/unit/session/test_io_bigquery.py @@ -345,7 +345,7 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) ), ( "SELECT `row_index`, `string_col` FROM `test_table` " - "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125+00:00') " + "FOR SYSTEM_TIME AS OF CAST('2024-05-14T12:42:36.125125+00:00' AS TIMESTAMP) " "WHERE `rowindex` NOT IN (0, 6) OR `string_col` IN ('Hello, World!', " "'こんにちは') LIMIT 123" ), @@ -374,7 +374,7 @@ def test_bq_schema_to_sql(schema: Iterable[bigquery.SchemaField], expected: str) string_col, FROM `test_table` AS t ) """ - "FOR SYSTEM_TIME AS OF TIMESTAMP('2024-05-14T12:42:36.125125+00:00') " + "FOR SYSTEM_TIME AS OF CAST('2024-05-14T12:42:36.125125+00:00' AS TIMESTAMP) " "WHERE `rowindex` < 4 AND `string_col` = 'Hello, World!' " "LIMIT 123" ), diff --git a/tests/unit/test_col.py b/tests/unit/test_col.py index c7a7eaa326c..c3fcb10c9d9 100644 --- a/tests/unit/test_col.py +++ b/tests/unit/test_col.py @@ -227,3 +227,22 @@ def test_getitem_with_pd_col(scalars_dfs): pd_result = scalars_pandas_df[pd.col("float64_col") > 4] # type: ignore assert_frame_equal(bf_result, pd_result) + + +def test_col_str_accessor(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.assign(result=bpd.col("string_col").str.lower()).to_pandas() + pd_result = scalars_pandas_df.assign(result=pd.col("string_col").str.lower()) # type: ignore + + assert_frame_equal(bf_result, pd_result) + + +def test_col_dt_accessor(scalars_dfs): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.assign(result=bpd.col("date_col").dt.year).to_pandas() + pd_result = scalars_pandas_df.assign(result=pd.col("date_col").dt.year) # type: ignore + + # int64[pyarrow] vs Int64 + assert_frame_equal(bf_result, pd_result, check_dtype=False) diff --git a/third_party/bigframes_vendored/ibis/expr/operations/ai_ops.py b/third_party/bigframes_vendored/ibis/expr/operations/ai_ops.py index ef387d33792..fe978e14536 100644 --- a/third_party/bigframes_vendored/ibis/expr/operations/ai_ops.py +++ b/third_party/bigframes_vendored/ibis/expr/operations/ai_ops.py @@ -113,7 +113,7 @@ class AIIf(Value): """Generate True/False based on the prompt""" prompt: Value - connection_id: Value[dt.String] + connection_id: Optional[Value[dt.String]] shape = rlz.shape_like("prompt") @@ -128,7 +128,7 @@ class AIClassify(Value): input: Value categories: Value[dt.Array[dt.String]] - connection_id: Value[dt.String] + connection_id: Optional[Value[dt.String]] shape = rlz.shape_like("input") @@ -142,7 +142,7 @@ class AIScore(Value): """Generate doubles based on the prompt""" prompt: Value - connection_id: Value[dt.String] + connection_id: Optional[Value[dt.String]] shape = rlz.shape_like("prompt") diff --git a/third_party/bigframes_vendored/sqlglot/dialects/dialect.py b/third_party/bigframes_vendored/sqlglot/dialects/dialect.py index 8dbb5c3f1c2..3341f3fa57b 100644 --- a/third_party/bigframes_vendored/sqlglot/dialects/dialect.py +++ b/third_party/bigframes_vendored/sqlglot/dialects/dialect.py @@ -166,7 +166,7 @@ def _try_load(cls, key: str | Dialects) -> None: # files. Custom user dialects need to be imported at the top-level package, in # order for them to be registered as soon as possible. if key in DIALECT_MODULE_NAMES: - importlib.import_module(f"sqlglot.dialects.{key}") + importlib.import_module(f"bigframes_vendored.sqlglot.dialects.{key}") @classmethod def __getitem__(cls, key: str) -> t.Type[Dialect]: diff --git a/third_party/bigframes_vendored/tpch/queries/q19.py b/third_party/bigframes_vendored/tpch/queries/q19.py index 1371af53fc0..a217db3dc32 100644 --- a/third_party/bigframes_vendored/tpch/queries/q19.py +++ b/third_party/bigframes_vendored/tpch/queries/q19.py @@ -53,5 +53,11 @@ def q(project_id: str, dataset_id: str, session: bigframes.Session): ) ] - revenue = (filtered["L_EXTENDEDPRICE"] * (1 - filtered["L_DISCOUNT"])).sum() - _ = round(revenue, 2) + result_df = ( + (filtered["L_EXTENDEDPRICE"] * (1 - filtered["L_DISCOUNT"])) + .agg(["sum"]) + .rename("REVENUE") + .to_frame() + ) + + next(result_df.to_pandas_batches(max_results=1500)) diff --git a/third_party/bigframes_vendored/version.py b/third_party/bigframes_vendored/version.py index 012a4502914..4928dd5c209 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.37.0" +__version__ = "2.38.0" # {x-release-please-start-date} -__release_date__ = "2026-03-03" +__release_date__ = "2026-03-16" # {x-release-please-end}