From eb496d999db872fb254c199ddca26171d75d67a0 Mon Sep 17 00:00:00 2001 From: jialuoo Date: Wed, 19 Mar 2025 13:15:37 -0700 Subject: [PATCH 01/39] chore: remove unused import in function.py (#1502) --- bigframes/functions/function.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bigframes/functions/function.py b/bigframes/functions/function.py index fd2f512f97..30b3d23056 100644 --- a/bigframes/functions/function.py +++ b/bigframes/functions/function.py @@ -35,7 +35,6 @@ import bigframes.dtypes import bigframes.exceptions as bfe import bigframes.formatting_helpers as bf_formatting -import bigframes.functions.function_template from . import _function_session as bff_session from . import _utils From a9cf215fb1403fda4ab2b58252f5fedc33aba3e1 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:20:07 -0700 Subject: [PATCH 02/39] feat: Allow iloc to support lists of negative indices (#1497) * feat: support iloc with negative indices * update partial ordering test * update naming * update logic * update comment * update logic and tests * update filter --- bigframes/core/indexers.py | 36 +++++++++++++++++++++++++++- tests/system/small/test_dataframe.py | 24 ++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/bigframes/core/indexers.py b/bigframes/core/indexers.py index d1a0c42e97..6258eb00d5 100644 --- a/bigframes/core/indexers.py +++ b/bigframes/core/indexers.py @@ -27,6 +27,7 @@ import bigframes.core.guid as guid import bigframes.core.indexes as indexes import bigframes.core.scalar +import bigframes.core.window_spec as windows import bigframes.dataframe import bigframes.dtypes import bigframes.exceptions as bfe @@ -477,6 +478,19 @@ def _iloc_getitem_series_or_dataframe( Union[bigframes.dataframe.DataFrame, bigframes.series.Series], series_or_dataframe.iloc[0:0], ) + + # Check if both positive index and negative index are necessary + if isinstance(key, (bigframes.series.Series, indexes.Index)): + # Avoid data download + is_key_unisigned = False + else: + first_sign = key[0] >= 0 + is_key_unisigned = True + for k in key: + if (k >= 0) != first_sign: + is_key_unisigned = False + break + if isinstance(series_or_dataframe, bigframes.series.Series): original_series_name = series_or_dataframe.name series_name = ( @@ -497,7 +511,27 @@ def _iloc_getitem_series_or_dataframe( block = df._block # explicitly set index to offsets, reset_index may not generate offsets in some modes block, offsets_id = block.promote_offsets("temp_iloc_offsets_") - block = block.set_index([offsets_id]) + pos_block = block.set_index([offsets_id]) + + if not is_key_unisigned or key[0] < 0: + neg_block, size_col_id = block.apply_window_op( + offsets_id, + ops.aggregations.SizeUnaryOp(), + window_spec=windows.rows(), + ) + neg_block, neg_index_id = neg_block.apply_binary_op( + offsets_id, size_col_id, ops.SubOp() + ) + + neg_block = neg_block.set_index([neg_index_id]).drop_columns( + [size_col_id, offsets_id] + ) + + if is_key_unisigned: + block = pos_block if key[0] >= 0 else neg_block + else: + block = pos_block.concat([neg_block], how="inner") + df = bigframes.dataframe.DataFrame(block) result = df.loc[key] diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index b97846d992..c2e4a1c8ad 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -4400,9 +4400,15 @@ def test_loc_list_multiindex(scalars_dfs_maybe_ordered): ) -def test_iloc_list(scalars_df_index, scalars_pandas_df_index): - index_list = [0, 0, 0, 5, 4, 7] - +@pytest.mark.parametrize( + "index_list", + [ + [0, 1, 2, 3, 4, 4], + [0, 0, 0, 5, 4, 7, -2, -5, 3], + [-1, -2, -3, -4, -5, -5], + ], +) +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] @@ -4412,11 +4418,17 @@ def test_iloc_list(scalars_df_index, scalars_pandas_df_index): ) +@pytest.mark.parametrize( + "index_list", + [ + [0, 1, 2, 3, 4, 4], + [0, 0, 0, 5, 4, 7, -2, -5, 3], + [-1, -2, -3, -4, -5, -5], + ], +) def test_iloc_list_partial_ordering( - scalars_df_partial_ordering, scalars_pandas_df_index + scalars_df_partial_ordering, scalars_pandas_df_index, index_list ): - index_list = [0, 0, 0, 5, 4, 7] - bf_result = scalars_df_partial_ordering.iloc[index_list] pd_result = scalars_pandas_df_index.iloc[index_list] From 75fc7e0268dc5b10bdbc33dcf28db97dce62e41c Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Wed, 19 Mar 2025 15:11:34 -0700 Subject: [PATCH 03/39] feat: Support dry_run in `to_pandas()` (#1436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Support dry_run in * centralize dry_run logics at block level * fix lint errors * remove unnecessary code * use dataframe for dry_run stats * flatten the job stats to a series * fix lint * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix query job issue * Make pandas surface directly call block._compute_dry_run * type hint update --------- Co-authored-by: Owl Bot --- bigframes/core/blocks.py | 120 ++++++++++++++++++++---- bigframes/core/indexes/base.py | 44 +++++++-- bigframes/dataframe.py | 51 +++++++++- bigframes/series.py | 27 +++++- tests/system/small/test_dataframe_io.py | 8 ++ tests/system/small/test_index.py | 9 ++ tests/system/small/test_series.py | 8 ++ tests/unit/core/test_blocks.py | 8 ++ 8 files changed, 243 insertions(+), 32 deletions(-) diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index db6007b41a..2992718412 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -22,6 +22,7 @@ from __future__ import annotations import ast +import copy import dataclasses import datetime import functools @@ -30,6 +31,7 @@ import textwrap import typing from typing import ( + Any, Iterable, List, Literal, @@ -49,7 +51,7 @@ import pyarrow as pa from bigframes import session -import bigframes._config.sampling_options as sampling_options +from bigframes._config import sampling_options import bigframes.constants import bigframes.core as core import bigframes.core.compile.googlesql as googlesql @@ -535,19 +537,9 @@ def to_pandas( Returns: pandas.DataFrame, QueryJob """ - if (sampling_method is not None) and (sampling_method not in _SAMPLING_METHODS): - raise NotImplementedError( - f"The downsampling method {sampling_method} is not implemented, " - f"please choose from {','.join(_SAMPLING_METHODS)}." - ) - - sampling = bigframes.options.sampling.with_max_download_size(max_download_size) - if sampling_method is not None: - sampling = sampling.with_method(sampling_method).with_random_state( # type: ignore - random_state - ) - else: - sampling = sampling.with_disabled() + sampling = self._get_sampling_option( + max_download_size, sampling_method, random_state + ) df, query_job = self._materialize_local( materialize_options=MaterializationOptions( @@ -559,6 +551,27 @@ def to_pandas( df.set_axis(self.column_labels, axis=1, copy=False) return df, query_job + def _get_sampling_option( + self, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, + ) -> sampling_options.SamplingOptions: + + if (sampling_method is not None) and (sampling_method not in _SAMPLING_METHODS): + raise NotImplementedError( + f"The downsampling method {sampling_method} is not implemented, " + f"please choose from {','.join(_SAMPLING_METHODS)}." + ) + + sampling = bigframes.options.sampling.with_max_download_size(max_download_size) + if sampling_method is None: + return sampling.with_disabled() + + return sampling.with_method(sampling_method).with_random_state( # type: ignore + random_state + ) + def try_peek( self, n: int = 20, force: bool = False, allow_large_results=None ) -> typing.Optional[pd.DataFrame]: @@ -798,11 +811,73 @@ def split( return [sliced_block.drop_columns(drop_cols) for sliced_block in sliced_blocks] def _compute_dry_run( - self, value_keys: Optional[Iterable[str]] = None - ) -> bigquery.QueryJob: + self, + value_keys: Optional[Iterable[str]] = None, + *, + ordered: bool = True, + max_download_size: Optional[int] = None, + sampling_method: Optional[str] = None, + random_state: Optional[int] = None, + ) -> typing.Tuple[pd.Series, bigquery.QueryJob]: + sampling = self._get_sampling_option( + max_download_size, sampling_method, random_state + ) + if sampling.enable_downsampling: + raise NotImplementedError("Dry run with sampling is not supported") + + index: List[Any] = [] + values: List[Any] = [] + + index.append("columnCount") + values.append(len(self.value_columns)) + index.append("columnDtypes") + values.append( + { + col: self.expr.get_column_type(self.resolve_label_exact_or_error(col)) + for col in self.column_labels + } + ) + + index.append("indexLevel") + values.append(self.index.nlevels) + index.append("indexDtypes") + values.append(self.index.dtypes) + expr = self._apply_value_keys_to_expr(value_keys=value_keys) - query_job = self.session._executor.dry_run(expr) - return query_job + query_job = self.session._executor.dry_run(expr, ordered) + job_api_repr = copy.deepcopy(query_job._properties) + + job_ref = job_api_repr["jobReference"] + for key, val in job_ref.items(): + index.append(key) + values.append(val) + + index.append("jobType") + values.append(job_api_repr["configuration"]["jobType"]) + + query_config = job_api_repr["configuration"]["query"] + for key in ("destinationTable", "useLegacySql"): + index.append(key) + values.append(query_config.get(key)) + + query_stats = job_api_repr["statistics"]["query"] + for key in ( + "referencedTables", + "totalBytesProcessed", + "cacheHit", + "statementType", + ): + index.append(key) + values.append(query_stats.get(key)) + + index.append("creationTime") + values.append( + pd.Timestamp( + job_api_repr["statistics"]["creationTime"], unit="ms", tz="UTC" + ) + ) + + return pd.Series(values, index=index), query_job def _apply_value_keys_to_expr(self, value_keys: Optional[Iterable[str]] = None): expr = self._expr @@ -2703,11 +2778,18 @@ def to_pandas( "Cannot materialize index, as this object does not have an index. Set index column(s) using set_index." ) ordered = ordered if ordered is not None else True + df, query_job = self._block.select_columns([]).to_pandas( - ordered=ordered, allow_large_results=allow_large_results + ordered=ordered, + allow_large_results=allow_large_results, ) return df.index, query_job + def _compute_dry_run( + self, *, ordered: bool = True + ) -> Tuple[pd.Series, bigquery.QueryJob]: + return self._block.select_columns([])._compute_dry_run(ordered=ordered) + def resolve_level(self, level: LevelsType) -> typing.Sequence[str]: if utils.is_list_like(level): levels = list(level) diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 84da6c5de0..900825996e 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -17,7 +17,7 @@ from __future__ import annotations import typing -from typing import Hashable, Literal, Optional, Sequence, Union +from typing import Hashable, Literal, Optional, overload, Sequence, Union import bigframes_vendored.constants as constants import bigframes_vendored.pandas.core.indexes.base as vendored_pandas_index @@ -228,7 +228,7 @@ def T(self) -> Index: return self.transpose() @property - def query_job(self) -> Optional[bigquery.QueryJob]: + def query_job(self) -> bigquery.QueryJob: """BigQuery job metadata for the most recent query. Returns: @@ -236,7 +236,8 @@ def query_job(self) -> Optional[bigquery.QueryJob]: `_. """ if self._query_job is None: - self._query_job = self._block._compute_dry_run() + _, query_job = self._block._compute_dry_run() + self._query_job = query_job return self._query_job def __repr__(self) -> str: @@ -252,7 +253,8 @@ def __repr__(self) -> str: opts = bigframes.options.display max_results = opts.max_rows if opts.repr_mode == "deferred": - return formatter.repr_query_job(self._block._compute_dry_run()) + _, dry_run_query_job = self._block._compute_dry_run() + return formatter.repr_query_job(dry_run_query_job) pandas_df, _, query_job = self._block.retrieve_repr_request_results(max_results) self._query_job = query_job @@ -490,18 +492,46 @@ def __getitem__(self, key: int) -> typing.Any: else: raise NotImplementedError(f"Index key not supported {key}") - def to_pandas(self, *, allow_large_results: Optional[bool] = None) -> pandas.Index: + @overload + def to_pandas( + self, + *, + allow_large_results: Optional[bool] = ..., + dry_run: Literal[False] = ..., + ) -> pandas.Index: + ... + + @overload + def to_pandas( + self, *, allow_large_results: Optional[bool] = ..., dry_run: Literal[True] = ... + ) -> pandas.Series: + ... + + def to_pandas( + self, *, allow_large_results: Optional[bool] = None, dry_run: bool = False + ) -> pandas.Index | pandas.Series: """Gets the Index as a pandas Index. Args: allow_large_results (bool, default None): If not None, overrides the global setting to allow or disallow large query results over the default size limit of 10 GB. + dry_run (bool, default False): + If this argument is true, this method will not process the data. Instead, it returns + a Pandas series containing dtype and the amount of bytes to be processed. Returns: - pandas.Index: - A pandas Index with all of the labels from this Index. + pandas.Index | pandas.Series: + A pandas Index with all of the labels from this Index. If dry run is set to True, + returns a Series containing dry run statistics. """ + if dry_run: + dry_run_stats, dry_run_job = self._block.index._compute_dry_run( + ordered=True + ) + self._query_job = dry_run_job + return dry_run_stats + df, query_job = self._block.index.to_pandas( ordered=True, allow_large_results=allow_large_results ) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index abab9fd268..6d1425617c 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -32,6 +32,7 @@ Literal, Mapping, Optional, + overload, Sequence, Tuple, Union, @@ -1594,6 +1595,32 @@ def to_arrow( self._set_internal_query_job(query_job) return pa_table + @overload + def to_pandas( + self, + max_download_size: Optional[int] = ..., + sampling_method: Optional[str] = ..., + random_state: Optional[int] = ..., + *, + ordered: bool = ..., + dry_run: Literal[False] = ..., + allow_large_results: Optional[bool] = ..., + ) -> pandas.DataFrame: + ... + + @overload + def to_pandas( + self, + max_download_size: Optional[int] = ..., + sampling_method: Optional[str] = ..., + random_state: Optional[int] = ..., + *, + ordered: bool = ..., + dry_run: Literal[True] = ..., + allow_large_results: Optional[bool] = ..., + ) -> pandas.Series: + ... + def to_pandas( self, max_download_size: Optional[int] = None, @@ -1601,8 +1628,9 @@ def to_pandas( random_state: Optional[int] = None, *, ordered: bool = True, + dry_run: bool = False, allow_large_results: Optional[bool] = None, - ) -> pandas.DataFrame: + ) -> pandas.DataFrame | pandas.Series: """Write DataFrame to pandas DataFrame. Args: @@ -1624,6 +1652,9 @@ def to_pandas( ordered (bool, default True): Determines whether the resulting pandas dataframe will be ordered. In some cases, unordered may result in a faster-executing query. + dry_run (bool, default False): + If this argument is true, this method will not process the data. Instead, it returns + a Pandas Series containing dry run statistics allow_large_results (bool, default None): If not None, overrides the global setting to allow or disallow large query results over the default size limit of 10 GB. @@ -1631,9 +1662,22 @@ def to_pandas( Returns: pandas.DataFrame: A pandas DataFrame with all rows and columns of this DataFrame if the data_sampling_threshold_mb is not exceeded; otherwise, a pandas DataFrame with - downsampled rows and all columns of this DataFrame. + downsampled rows and all columns of this DataFrame. If dry_run is set, a pandas + Series containing dry run statistics will be returned. """ + # TODO(orrbradford): Optimize this in future. Potentially some cases where we can return the stored query job + + if dry_run: + dry_run_stats, dry_run_job = self._block._compute_dry_run( + max_download_size=max_download_size, + sampling_method=sampling_method, + random_state=random_state, + ordered=ordered, + ) + self._set_internal_query_job(dry_run_job) + return dry_run_stats + df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -1679,7 +1723,8 @@ def to_pandas_batches( ) def _compute_dry_run(self) -> bigquery.QueryJob: - return self._block._compute_dry_run() + _, query_job = self._block._compute_dry_run() + return query_job def copy(self) -> DataFrame: return DataFrame(self._block) diff --git a/bigframes/series.py b/bigframes/series.py index 34ac3c3de9..b86b1ca6db 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -381,6 +381,7 @@ def to_pandas( random_state: Optional[int] = None, *, ordered: bool = True, + dry_run: bool = False, allow_large_results: Optional[bool] = None, ) -> pandas.Series: """Writes Series to pandas Series. @@ -404,15 +405,32 @@ def to_pandas( ordered (bool, default True): Determines whether the resulting pandas series will be ordered. In some cases, unordered may result in a faster-executing query. + dry_run (bool, default False): + If this argument is true, this method will not process the data. Instead, it returns + a Pandas Series containing dry run job statistics allow_large_results (bool, default None): If not None, overrides the global setting to allow or disallow large query results over the default size limit of 10 GB. - Returns: pandas.Series: A pandas Series with all rows of this Series if the data_sampling_threshold_mb - is not exceeded; otherwise, a pandas Series with downsampled rows of the DataFrame. + is not exceeded; otherwise, a pandas Series with downsampled rows of the DataFrame. If dry_run + is set to True, a pandas Series containing dry run statistics will be returned. """ + + if dry_run: + dry_run_stats, dry_run_job = self._block._compute_dry_run( + max_download_size=max_download_size, + sampling_method=sampling_method, + random_state=random_state, + ordered=ordered, + ) + + self._set_internal_query_job(dry_run_job) + return dry_run_stats + + # Repeat the to_pandas() call to make mypy deduce type correctly, because mypy cannot resolve + # Literal[True/False] to bool df, query_job = self._block.to_pandas( max_download_size=max_download_size, sampling_method=sampling_method, @@ -420,14 +438,17 @@ def to_pandas( ordered=ordered, allow_large_results=allow_large_results, ) + if query_job: self._set_internal_query_job(query_job) + series = df.squeeze(axis=1) series.name = self._name return series def _compute_dry_run(self) -> bigquery.QueryJob: - return self._block._compute_dry_run((self._value_column,)) + _, query_job = self._block._compute_dry_run((self._value_column,)) + return query_job def drop( self, diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index 30a78b5cdc..d2ba96b41f 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -927,3 +927,11 @@ def test_to_sql_query_named_index_excluded( utils.assert_pandas_df_equal( roundtrip.to_pandas(), pd_df, check_index_type=False, ignore_order=True ) + + +def test_to_pandas_dry_run(session, scalars_pandas_df_multi_index): + bf_df = session.read_pandas(scalars_pandas_df_multi_index) + + result = bf_df.to_pandas(dry_run=True) + + assert len(result) == 14 diff --git a/tests/system/small/test_index.py b/tests/system/small/test_index.py index 4d01bc5ee9..535e4bc9ae 100644 --- a/tests/system/small/test_index.py +++ b/tests/system/small/test_index.py @@ -24,6 +24,7 @@ def test_index_construct_from_list(): bf_result = bpd.Index( [3, 14, 159], dtype=pd.Int64Dtype(), name="my_index" ).to_pandas() + pd_result: pd.Index = pd.Index([3, 14, 159], dtype=pd.Int64Dtype(), name="my_index") pd.testing.assert_index_equal(bf_result, pd_result) @@ -425,3 +426,11 @@ def test_multiindex_repr_includes_all_names(session): ) index = session.read_pandas(df).set_index(["A", "B"]).index assert "names=['A', 'B']" in repr(index) + + +def test_to_pandas_dry_run(scalars_df_index): + index = scalars_df_index.index + + result = index.to_pandas(dry_run=True) + + assert len(result) == 14 diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index ef544b0a0b..f1f060caa4 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -4384,3 +4384,11 @@ def test_series_struct_class_attributes_shadow_struct_fields(nested_structs_df): series = nested_structs_df["person"] assert series.name == "person" + + +def test_series_to_pandas_dry_run(scalars_df_index): + bf_series = scalars_df_index["int64_col"] + + result = bf_series.to_pandas(dry_run=True) + + assert len(result) == 14 diff --git a/tests/unit/core/test_blocks.py b/tests/unit/core/test_blocks.py index 8ed3acba0f..fb5a927e76 100644 --- a/tests/unit/core/test_blocks.py +++ b/tests/unit/core/test_blocks.py @@ -92,3 +92,11 @@ def test_block_from_local(data): pandas.testing.assert_index_equal(block.column_labels, expected.columns) assert tuple(block.index.names) == tuple(expected.index.names) assert block.shape == expected.shape + + +def test_block_compute_dry_run__raises_error_when_sampling_is_enabled(): + mock_session = mock.create_autospec(spec=bigframes.Session) + block = blocks.Block.from_local(pandas.DataFrame(), mock_session) + + with pytest.raises(NotImplementedError): + block._compute_dry_run(sampling_method="UNIFORM") From 7d00be67cf50fdf713c40912f207d14f0f65538f Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:48:09 -0700 Subject: [PATCH 04/39] feat: add df.take and series.take (#1509) * feat: add df.take and series.take * update --- bigframes/dataframe.py | 12 ++++++++++ bigframes/series.py | 7 ++++++ tests/system/small/test_dataframe.py | 18 +++++++++++++++ tests/system/small/test_series.py | 17 ++++++++++++++ .../bigframes_vendored/pandas/core/generic.py | 23 +++++++++++++++++++ 5 files changed, 77 insertions(+) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 6d1425617c..38f663f56f 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2219,6 +2219,18 @@ def add_suffix(self, suffix: str, axis: int | str | None = None) -> DataFrame: axis = 1 if axis is None else axis return DataFrame(self._get_block().add_suffix(suffix, axis)) + def take( + self, indices: typing.Sequence[int], axis: int | str | None = 0, **kwargs + ) -> DataFrame: + if not utils.is_list_like(indices): + raise ValueError("indices should be a list-like object.") + if axis == 0 or axis == "index": + return self.iloc[indices] + elif axis == 1 or axis == "columns": + return self.iloc[:, indices] + else: + raise ValueError(f"No axis named {axis} for object type DataFrame") + def filter( self, items: typing.Optional[typing.Iterable] = None, diff --git a/bigframes/series.py b/bigframes/series.py index b86b1ca6db..5f49daa07d 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -1652,6 +1652,13 @@ def add_prefix(self, prefix: str, axis: int | str | None = None) -> Series: def add_suffix(self, suffix: str, axis: int | str | None = None) -> Series: return Series(self._get_block().add_suffix(suffix)) + def take( + self, indices: typing.Sequence[int], axis: int | str | None = 0, **kwargs + ) -> Series: + if not utils.is_list_like(indices): + raise ValueError("indices should be a list-like object.") + return typing.cast(Series, self.iloc[indices]) + def filter( self, items: typing.Optional[typing.Iterable] = None, diff --git a/tests/system/small/test_dataframe.py b/tests/system/small/test_dataframe.py index c2e4a1c8ad..8cc3be1577 100644 --- a/tests/system/small/test_dataframe.py +++ b/tests/system/small/test_dataframe.py @@ -807,6 +807,24 @@ def test_get_df_column_name_duplicate(scalars_dfs): pd.testing.assert_index_equal(bf_result.columns, pd_result.columns) +@pytest.mark.parametrize( + ("indices", "axis"), + [ + ([1, 3, 5], 0), + ([2, 4, 6], 1), + ([1, -3, -5, -6], "index"), + ([-2, -4, -6], "columns"), + ], +) +def test_take_df(scalars_dfs, indices, axis): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.take(indices, axis=axis).to_pandas() + pd_result = scalars_pandas_df.take(indices, axis=axis) + + assert_pandas_df_equal(bf_result, pd_result) + + def test_filter_df(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs diff --git a/tests/system/small/test_series.py b/tests/system/small/test_series.py index f1f060caa4..acd267aaf8 100644 --- a/tests/system/small/test_series.py +++ b/tests/system/small/test_series.py @@ -1543,6 +1543,23 @@ def test_indexing_using_selected_series(scalars_dfs): ) +@pytest.mark.parametrize( + ("indices"), + [ + ([1, 3, 5]), + ([5, -3, -5, -6]), + ([-2, -4, -6]), + ], +) +def test_take(scalars_dfs, indices): + scalars_df, scalars_pandas_df = scalars_dfs + + bf_result = scalars_df.take(indices).to_pandas() + pd_result = scalars_pandas_df.take(indices) + + assert_pandas_df_equal(bf_result, pd_result) + + def test_nested_filter(scalars_dfs): scalars_df, scalars_pandas_df = scalars_dfs string_col = scalars_df["string_col"] diff --git a/third_party/bigframes_vendored/pandas/core/generic.py b/third_party/bigframes_vendored/pandas/core/generic.py index ee35bfa429..8dd43fd8da 100644 --- a/third_party/bigframes_vendored/pandas/core/generic.py +++ b/third_party/bigframes_vendored/pandas/core/generic.py @@ -910,6 +910,29 @@ def notna(self) -> NDFrame: notnull = notna + def take(self, indices, axis=0, **kwargs) -> NDFrame: + """Return the elements in the given positional indices along an axis. + + This means that we are not indexing according to actual values in the index + attribute of the object. We are indexing according to the actual position of + the element in the object. + + Args: + indices (list-like): + An array of ints indicating which positions to take. + axis ({0 or 'index', 1 or 'columns', None}, default 0): + The axis on which to select elements. 0 means that we are selecting rows, + 1 means that we are selecting columns. For Series this parameter is + unused and defaults to 0. + **kwargs: + For compatibility with numpy.take(). Has no effect on the output. + + Returns: + bigframes.pandas.DataFrame or bigframes.pandas.Series: + Same type as input object. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + def filter( self, items=None, From 42917f7453363851eb0931c0928b43e11efa0483 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 20 Mar 2025 12:20:00 -0700 Subject: [PATCH 05/39] chore: Ignore mypy false positive (#1515) --- bigframes/core/indexes/base.py | 2 +- bigframes/dataframe.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bigframes/core/indexes/base.py b/bigframes/core/indexes/base.py index 900825996e..a90e0a8705 100644 --- a/bigframes/core/indexes/base.py +++ b/bigframes/core/indexes/base.py @@ -493,7 +493,7 @@ def __getitem__(self, key: int) -> typing.Any: raise NotImplementedError(f"Index key not supported {key}") @overload - def to_pandas( + def to_pandas( # type: ignore[overload-overlap] self, *, allow_large_results: Optional[bool] = ..., diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 38f663f56f..4955bb1295 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -1596,7 +1596,7 @@ def to_arrow( return pa_table @overload - def to_pandas( + def to_pandas( # type: ignore[overload-overlap] self, max_download_size: Optional[int] = ..., sampling_method: Optional[str] = ..., From dcfbb63c996229b0fa2b439769021837223b577b Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Thu, 20 Mar 2025 13:00:34 -0700 Subject: [PATCH 06/39] chore: runs 'pip freeze' in system tests for debugging (#1504) --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index 74538f49f0..77b32ab15d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -342,6 +342,9 @@ def run_system( install_systemtest_dependencies(session, install_test_extra, "-c", constraints_path) + # Print out package versions for debugging. + session.run("python", "-m", "pip", "freeze") + # Run py.test against the system tests. pytest_cmd = [ "py.test", From a70a607512797463f70ed529f078fcb2d40c85a1 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 20 Mar 2025 13:26:44 -0700 Subject: [PATCH 07/39] fix: change the default value for pdf extract/chunk (#1517) --- bigframes/operations/blob.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/bigframes/operations/blob.py b/bigframes/operations/blob.py index 5e786f8d22..ab529c1312 100644 --- a/bigframes/operations/blob.py +++ b/bigframes/operations/blob.py @@ -560,9 +560,9 @@ def pdf_extract( self, *, connection: Optional[str] = None, - max_batching_rows: int = 8192, - container_cpu: Union[float, int] = 0.33, - container_memory: str = "512Mi", + max_batching_rows: int = 1, + container_cpu: Union[float, int] = 2, + container_memory: str = "1Gi", ) -> bigframes.series.Series: """Extracts text from PDF URLs and saves the text as string. @@ -574,10 +574,10 @@ def pdf_extract( connection (str or None, default None): BQ connection used for function internet transactions, and the output blob if "dst" is str. If None, uses default connection of the session. - max_batching_rows (int, default 8,192): Max number of rows per batch + max_batching_rows (int, default 1): Max number of rows per batch send to cloud run to execute the function. - container_cpu (int or float, default 0.33): number of container CPUs. Possible values are [0.33, 8]. Floats larger than 1 are cast to intergers. - container_memory (str, default "512Mi"): container memory size. String of the format . Possible values are from 512Mi to 32Gi. + container_cpu (int or float, default 2): number of container CPUs. Possible values are [0.33, 8]. Floats larger than 1 are cast to intergers. + container_memory (str, default "1Gi"): container memory size. String of the format . Possible values are from 512Mi to 32Gi. Returns: bigframes.series.Series: conatins all text from a pdf file @@ -604,11 +604,11 @@ def pdf_chunk( self, *, connection: Optional[str] = None, - chunk_size: int = 1000, + chunk_size: int = 2000, overlap_size: int = 200, - max_batching_rows: int = 8192, - container_cpu: Union[float, int] = 0.33, - container_memory: str = "512Mi", + max_batching_rows: int = 1, + container_cpu: Union[float, int] = 2, + container_memory: str = "1Gi", ) -> bigframes.series.Series: """Extracts and chunks text from PDF URLs and saves the text as arrays of strings. @@ -620,15 +620,15 @@ def pdf_chunk( connection (str or None, default None): BQ connection used for function internet transactions, and the output blob if "dst" is str. If None, uses default connection of the session. - chunk_size (int, default 1000): the desired size of each text chunk + chunk_size (int, default 2000): the desired size of each text chunk (number of characters). overlap_size (int, default 200): the number of overlapping characters between consective chunks. The helps to ensure context is perserved across chunk boundaries. - max_batching_rows (int, default 8,192): Max number of rows per batch + max_batching_rows (int, default 1): Max number of rows per batch send to cloud run to execute the function. - container_cpu (int or float, default 0.33): number of container CPUs. Possible values are [0.33, 8]. Floats larger than 1 are cast to intergers. - container_memory (str, default "512Mi"): container memory size. String of the format . Possible values are from 512Mi to 32Gi. + container_cpu (int or float, default 2): number of container CPUs. Possible values are [0.33, 8]. Floats larger than 1 are cast to intergers. + container_memory (str, default "1Gi"): container memory size. String of the format . Possible values are from 512Mi to 32Gi. Returns: bigframe.series.Series: Series of array[str], where each string is a From 6681f1f9e30ed2325b85668de8a0b1d3d0e2858b Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Thu, 20 Mar 2025 13:53:28 -0700 Subject: [PATCH 08/39] fix: tolerate BQ connection service account propagation delay (#1505) --- bigframes/clients.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bigframes/clients.py b/bigframes/clients.py index c6e1d47909..1b8212377d 100644 --- a/bigframes/clients.py +++ b/bigframes/clients.py @@ -94,16 +94,24 @@ def create_bq_connection( # https://cloud.google.com/bigquery/docs/reference/standard-sql/remote-functions#grant_permission_on_function self._ensure_iam_binding(project_id, service_account_id, iam_role) - # Introduce retries to accommodate transient errors like etag mismatch, - # which can be caused by concurrent operation on the same resource, and - # manifests with message like: - # google.api_core.exceptions.Aborted: 409 There were concurrent policy - # changes. Please retry the whole read-modify-write with exponential - # backoff. The request's ETag '\007\006\003,\264\304\337\272' did not match - # the current policy's ETag '\007\006\003,\3750&\363'. + # Introduce retries to accommodate transient errors like: + # (1) Etag mismatch, + # which can be caused by concurrent operation on the same resource, and + # manifests with message like: + # google.api_core.exceptions.Aborted: 409 There were concurrent policy + # changes. Please retry the whole read-modify-write with exponential + # backoff. The request's ETag '\007\006\003,\264\304\337\272' did not + # match the current policy's ETag '\007\006\003,\3750&\363'. + # (2) Connection creation, + # for which sometimes it takes a bit for its service account to reflect + # across APIs (e.g. b/397662004, b/386838767), before which, an attempt + # to set an IAM policy for the service account may throw an error like: + # google.api_core.exceptions.InvalidArgument: 400 Service account + # bqcx-*@gcp-sa-bigquery-condel.iam.gserviceaccount.com does not exist. @google.api_core.retry.Retry( predicate=google.api_core.retry.if_exception_type( - google.api_core.exceptions.Aborted + google.api_core.exceptions.Aborted, + google.api_core.exceptions.InvalidArgument, ), initial=10, maximum=20, From 2818ab9e19060b24bef2bab5dcc6fda333e687c6 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Thu, 20 Mar 2025 14:06:57 -0700 Subject: [PATCH 09/39] chore: Covid notebook uses partial ordering mode (#1518) --- .../bq_dataframes_covid_line_graphs.ipynb | 75 +++++++++++-------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb index c3b4c8e616..b3ae35f013 100644 --- a/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb +++ b/notebooks/visualization/bq_dataframes_covid_line_graphs.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 16, "metadata": { "id": "9GIt_orUtNvA" }, @@ -135,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 17, "metadata": { "id": "4aooKMmnxrWF" }, @@ -157,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 18, "metadata": { "id": "bk03Rt_HyGx-" }, @@ -206,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 19, "metadata": { "id": "R7STCS8xB5d2" }, @@ -222,7 +222,9 @@ "# 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" + "bpd.options.bigquery.location = REGION\n", + "# Improves performance by avoiding generating total row ordering\n", + "bpd.options.bigquery.ordering_mode = \"partial\"" ] }, { @@ -236,11 +238,23 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 20, "metadata": { "id": "zDSwoBo1CU3G" }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/tbergeron/src/bigframes/venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py:3579: UserWarning: Reading cached table from 2025-03-20 20:22:07.633084+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", + " exec(code_obj, self.user_global_ns, self.user_ns)\n" + ] + } + ], "source": [ "all_data = bpd.read_gbq(\"bigquery-public-data.covid19_open_data.covid19_open_data\")" ] @@ -256,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 21, "metadata": { "id": "UjMT_qhjf8Fu" }, @@ -276,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 22, "metadata": { "id": "IaoUf57ZwrJ8" }, @@ -306,7 +320,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 23, "metadata": { "id": "tYDoaKgJChiq" }, @@ -336,7 +350,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 24, "metadata": { "id": "gFbCgfFC2gHw" }, @@ -344,7 +358,7 @@ { "data": { "text/html": [ - "Query job 307ec006-490f-435d-b3e3-74eb1d73fe0f is DONE. 372.9 MB processed. Open Job" + "Query job e8946d0f-20f1-49ae-9af5-5136f45e792d is DONE. 372.9 MB processed. Open Job" ], "text/plain": [ "" @@ -359,13 +373,13 @@ "" ] }, - "execution_count": 9, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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zNCGrMHghoiIFGFqWIqTpcgeGNwQEZEiygMVc3tLmVyh2KEtIrWwObiZNWsWtm7disLCQke0h4iIbjFaM/tJcXViqi6bg5vdu3fj7rvvRnBwMPr374/XXnsNf/75JwoKChzRPiIiUqmKFYorjhkHNMbxzemUHMxck4CULH7AJvNsDm42b96MzMxMbNmyBXfeeSfi4+MxZswYBAcHo1+/fo5oIxERqVBFzY3169x8H38RK/cm49nvDjmyaeTibF7nBgDc3d3Rt29f1KtXD6GhoQgICMDatWtx6tQpe7ePiIhUznizzIrjGpirsjlxNduhbSLXZnPm5vPPP8f999+PRo0aISYmBhs3bkS/fv0QHx+P9PR0R7SRiIhUyOT2C4q0hNTG5szNk08+iXr16uH555/HU089BX9/f0e0i4iIVM7UsFSVG2caHst5U2SezZmbn3/+GQ888ABWrVqFevXqISYmBjNnzsSmTZuQn5/viDYSEZEKlRcUW9o4kzOmqDpsztyMHj0ao0ePBgBkZWXh77//xurVq3HXXXdBq9VyijgREdnI9PRvBjZUXdUqKM7IyMC2bdsQFxeHuLg4HD9+HCEhIejfv7+920dERGplauNMK6tuOChFltgc3HTq1AknT55ESEgIbrvtNkyePBkDBgxA586dHdE+IiJSKZMFxczWkB1Uq6B4wIAB6NixoyPaQ0REtxhz2y9YwnpissTm4Obpp58GABQXF+PcuXNo0aIF3N2rNbpFRES3sPIZT+bqbDSwvNYNkTk2z5YqKCjAY489Bl9fX3To0AHJyckAgGeeeQazZ8+2ewOJiEh9UrMLkVesA1C9qeBEltgc3Lzyyis4cuQI4uLi4O3tbTg+ePBgfP/993ZtHBERqU9KViF6v7cFm0+kAjC/E7jGQqAjmM0hC2weT1q7di2+//579OnTR/bC69ChA86cOWPXxhERkfrsPZch+15jZldwS1hzQ5bYnLlJT09HWFhYpeN5eXlWvyiJiOjW5ePhJvtea+bSYemKwtiGLLE5uOnRowd+/fVXw/flAc0XX3yB6Oho+7WMiIhUybtScGOh5oafmakabB6Weu+99zBixAicOHECpaWlWLBgAU6cOIFdu3Zh27ZtjmgjERGpmLldwYmqy+bMTb9+/XD48GGUlpaiU6dO2LRpE8LCwrB79250797dEW0kIiIV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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHkCAYAAADCag6yAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfvpJREFUeJzt3Xd8U1X/B/BP0r0HUAq07L3LbgEBZYoKD4/ggwMcoD6KgjhBxcdZFBFQ+KGigqCIojJERRApyKbMsoplldVBoXsn5/dHaXpvmqRJm/Qml8/79eqL9OYmPYemud98z/ecoxFCCBARERGphFbpBhARERHZE4MbIiIiUhUGN0RERKQqDG6IiIhIVRjcEBERkaowuCEiIiJVYXBDREREqsLghoiIiFSFwQ0RERGpCoMbIiIiUpVbOrjZvn077r77bjRs2BAajQZr1661+TmEEPjwww/RunVreHl5oVGjRnj33Xft31giIiKyirvSDVBSXl4eunTpgkcffRRjxoyp1nNMnToVmzZtwocffohOnTrh+vXruH79up1bSkRERNbScOPMMhqNBmvWrMHo0aMNx4qKivDqq6/iu+++Q2ZmJjp27Ij3338fAwcOBACcPHkSnTt3xrFjx9CmTRtlGk5EREQyt/SwVFWmTJmC3bt3Y9WqVTh69CjGjh2L4cOH459//gEA/PLLL2jevDk2bNiAZs2aoWnTppg0aRIzN0RERApicGNGcnIyli5ditWrV6N///5o0aIFXnjhBfTr1w9Lly4FAJw9exYXLlzA6tWrsXz5cixbtgwHDhzAvffeq3DriYiIbl23dM2NJQkJCdDpdGjdurXseFFREerUqQMA0Ov1KCoqwvLlyw3nffnll+jevTsSExM5VEVERKQABjdm5Obmws3NDQcOHICbm5vsPn9/fwBAgwYN4O7uLguA2rVrB6As88PghoiIqPYxuDEjKioKOp0OaWlp6N+/v8lz+vbti9LSUpw5cwYtWrQAAJw+fRoA0KRJk1prKxEREVW4pWdL5ebmIikpCUBZMPPRRx9h0KBBCA0NRePGjfHggw9i586dmDt3LqKiopCeno4tW7agc+fOGDlyJPR6PXr27Al/f3/Mnz8fer0eTz/9NAIDA7Fp0yaFe0dERHRruqWDm7i4OAwaNKjS8YkTJ2LZsmUoKSnBO++8g+XLl+Py5cuoW7cu+vTpgzfffBOdOnUCAFy5cgXPPPMMNm3aBD8/P4wYMQJz585FaGhobXeHiIiIcIsHN0RERKQ+nApOREREqnLLFRTr9XpcuXIFAQEB0Gg0SjeHiIiIrCCEQE5ODho2bAit1nJu5pYLbq5cuYLIyEilm0FERETVcPHiRURERFg855YLbgICAgCU/ecEBgYq3BoiIiKyRnZ2NiIjIw3XcUtuueCmfCgqMDCQwQ0REZGLsaakhAXFREREpCoMboiIiEhVGNwQERGRqtxyNTfW0ul0KCkpUboZpFIeHh6VNmQlIiL7YHBjRAiBlJQUZGZmKt0UUrng4GCEh4dzvSUiIjtjcGOkPLAJCwuDr68vLzxkd0II5OfnIy0tDQDQoEEDhVtERKQuDG4kdDqdIbCpU6eO0s0hFfPx8QEApKWlISwsjENURER2xIJiifIaG19fX4VbQreC8tcZa7uIiOyLwY0JHIqi2sDXGRGRYzC4ISIiIlVxmuBm9uzZ0Gg0mDZtmsXzVq9ejbZt28Lb2xudOnXCb7/9VjsNJCIiIpfgFMHN/v378dlnn6Fz584Wz9u1axfGjx+Pxx57DIcOHcLo0aMxevRoHDt2rJZaSs5i586d6NSpEzw8PDB69GjExcVBo9E41RT+pk2bYv78+Uo3g4jolqN4cJObm4sHHngAS5YsQUhIiMVzFyxYgOHDh+PFF19Eu3bt8Pbbb6Nbt25YuHBhLbWWnMX06dPRtWtXnDt3DsuWLUNMTAyuXr2KoKAgpZtGREQKUzy4efrppzFy5EgMHjy4ynN3795d6bxhw4Zh9+7dZh9TVFSE7Oxs2Re5vjNnzuD2229HREQEgoOD4enpaXFBPJ1OB71eX8utJCJr6fUCM9ckYOXeZKWbQiqgaHCzatUqHDx4ELGxsVadn5KSgvr168uO1a9fHykpKWYfExsbi6CgIMNXZGSkTW0UQiC/uFSRLyGE1e0cOHAgnn32Wbz00ksIDQ1FeHg4/ve//xnuz8zMxKRJk1CvXj0EBgbi9ttvx5EjRwAAWVlZcHNzQ3x8PABAr9cjNDQUffr0MTz+m2++sfr/7tKlSxg/fjxCQ0Ph5+eHHj16YO/evYb7Fy9ejBYtWsDT0xNt2rTBihUrZI/XaDT44osv8K9//Qu+vr5o1aoV1q9fDwA4f/48NBoNMjIy8Oijj0Kj0WDZsmWVhqWWLVuG4OBgrF+/Hu3bt4eXlxeSk5PRtGlTvPPOO5gwYQL8/f3RpEkTrF+/Hunp6Rg1ahT8/f3RuXNnw/9FuR07dqB///7w8fFBZGQknn32WeTl5RnuT0tLw9133w0fHx80a9YM3377rVX/V0RUZtvpdKzcm4yZaxKUbgqpgGKL+F28eBFTp07F5s2b4e3t7bCfM2PGDEyfPt3wfXZ2tk0BTkGJDu1n/eGIplXpxFvD4Otp/a/o66+/xvTp07F3717s3r0bDz/8MPr27YshQ4Zg7Nix8PHxwe+//46goCB89tlnuOOOO3D69GmEhoaia9euiIuLQ48ePZCQkACNRoNDhw4hNzcX/v7+2LZtGwYMGFBlG3JzczFgwAA0atQI69evR3h4OA4ePGjImqxZswZTp07F/PnzMXjwYGzYsAGPPPIIIiIiMGjQIMPzvPnmm/jggw8wZ84cfPLJJ3jggQdw4cIFREZG4urVq2jTpg3eeust3HfffQgKCpIFT+Xy8/Px/vvv44svvkCdOnUQFhYGAJg3bx7ee+89vP7665g3bx4eeughxMTE4NFHH8WcOXPw8ssvY8KECTh+/Dg0Gg3OnDmD4cOH45133sFXX32F9PR0TJkyBVOmTMHSpUsBAA8//DCuXLmCrVu3wsPDA88++6xhBWIiqlpWAdd7IvtRLLg5cOAA0tLS0K1bN8MxnU6H7du3Y+HChSgqKqq0amt4eDhSU1Nlx1JTUxEeHm7253h5ecHLy8u+jXdSnTt3xhtvvAEAaNWqFRYuXIgtW7bAx8cH+/btQ1pamuH/4sMPP8TatWvx448/4vHHH8fAgQMRFxeHF154AXFxcRgyZAhOnTqFHTt2YPjw4YiLi8NLL71UZRtWrlyJ9PR07N+/H6GhoQCAli1bGu7/8MMP8fDDD+Opp54CUFY7s2fPHnz44Yey4Obhhx/G+PHjAQDvvfcePv74Y+zbtw/Dhw83DD8FBQVZ/N2XlJTg//7v/9ClSxfZ8TvvvBNPPPEEAGDWrFlYvHgxevbsibFjxwIAXn75ZURHRxteW7GxsXjggQcMM/latWqFjz/+GAMGDMDixYuRnJyM33//Hfv27UPPnj0BAF9++SXatWtX5f8XEZXhsk9kT4oFN3fccQcSEuTpx0ceeQRt27bFyy+/bHI5+ujoaGzZskU2XXzz5s2Ijo52WDt9PNxw4q1hDnv+qn62LYxnmzVo0ABpaWk4cuQIcnNzK20pUVBQgDNnzgAABgwYgC+//BI6nQ7btm3D0KFDER4ejri4OHTu3BlJSUkYOHBglW04fPgwoqKiDIGNsZMnT+Lxxx+XHevbty8WLFhgti9+fn4IDAy0ORPi6elpcgae9Fj5MGenTp0qHUtLS0N4eDiOHDmCo0ePyoaahBDQ6/U4d+4cTp8+DXd3d3Tv3t1wf9u2bREcHGxTe4mIyD4UC24CAgLQsWNH2TE/Pz/UqVPHcHzChAlo1KiRoSZn6tSpGDBgAObOnYuRI0di1apViI+Px+eff+6wdmo0GpuGhpTk4eEh+16j0UCv1yM3NxcNGjRAXFxcpceUX4Bvu+025OTk4ODBg9i+fTvee+89hIeHY/bs2ejSpQsaNmyIVq1aVdmG8j2TaspcX2zh4+NjssBY+tzl95s6Vv7zcnNz8cQTT+DZZ5+t9FyNGzfG6dOnbWoXERE5llNftZOTk6HVVtQ8x8TEYOXKlXjttdcwc+ZMtGrVCmvXrq0UJJFct27dkJKSAnd3dzRt2tTkOcHBwejcuTMWLlwIDw8PtG3bFmFhYbjvvvuwYcMGq+ptgLKsyBdffIHr16+bzN60a9cOO3fuxMSJEw3Hdu7cifbt21erb7WhW7duOHHihGx4Tapt27YoLS3FgQMHDMNSiYmJTrXmDhHRrcSpghvjzIKpTMPYsWMNtRFkncGDByM6OhqjR4/GBx98gNatW+PKlSv49ddf8a9//Qs9evQAUDbj6pNPPsG9994LAAgNDUW7du3w/fffY9GiRVb9rPHjx+O9997D6NGjERsbiwYNGuDQoUNo2LAhoqOj8eKLL2LcuHGIiorC4MGD8csvv+Dnn3/Gn3/+6bD+19TLL7+MPn36YMqUKZg0aRL8/Pxw4sQJbN68GQsXLkSbNm0wfPhwPPHEE1i8eDHc3d0xbdo0u2WxiIjINoqvc0OOp9Fo8Ntvv+G2227DI488gtatW+M///kPLly4IJtaP2DAAOh0OlltzcCBAysds8TT0xObNm1CWFgY7rzzTnTq1AmzZ8821FCNHj0aCxYswIcffogOHTrgs88+w9KlS61+fiV07twZ27Ztw+nTp9G/f39ERUVh1qxZaNiwoeGcpUuXomHDhhgwYADGjBmDxx9/3DA7i4iIapdG2LKYigpkZ2cjKCgIWVlZCAwMlN1XWFiIc+fOoVmzZg6dnk4E8PVGJLX+yBU8+90hAMD52SMVbg05I0vXb2PM3BAREZGqMLghm7z33nvw9/c3+TVixAilm0dERORcBcXk/J588kmMGzfO5H0soCWi6uIafmRPDG7IJqGhoWYX6CMiInIGHJYy4RarsSaF8HVGVIHbL5A9MbiRKF+lNj8/X+GW0K2g/HVmvBozERHVDIelJNzc3BAcHGzYw8jX19fk8v1ENSGEQH5+PtLS0hAcHGxyHzUiIqo+BjdGyneZtnWTRiJbBQcHW9zVnIiIqofBjRGNRoMGDRogLCwMJSUlSjeHVMrDw4MZGyIJDedLkR0xuDHDzc2NFx8iIiIXxIJiIiIiUhUGN0RERKQqDG6IiEhxnJhK9sTghoiIiFSFwQ0RESmOiRuyJwY3REREpCoMboiIiEhVGNwQEZHiWFBM9sTghoiIiFSFwQ0RETkBpm7IfhjcEBERkaowuCEiIiJVYXBDRERORQihdBPIxTG4ISIixUlnSzG2oZpicENERE6FsQ3VFIMbIiIiUhUGN0RE5FRYc0M1xeCGiIgUJ13lhqEN1RSDGyIicipM3FBNKRrcLF68GJ07d0ZgYCACAwMRHR2N33//3ez5y5Ytg0ajkX15e3vXYouJiMgRNJLpUoK5G6ohdyV/eEREBGbPno1WrVpBCIGvv/4ao0aNwqFDh9ChQweTjwkMDERiYqLhew13WyMiIiIJRYObu+++W/b9u+++i8WLF2PPnj1mgxuNRoPw8PDaaB4RESmAw1JUU05Tc6PT6bBq1Srk5eUhOjra7Hm5ublo0qQJIiMjMWrUKBw/ftzi8xYVFSE7O1v2RUREzoU5eLInxYObhIQE+Pv7w8vLC08++STWrFmD9u3bmzy3TZs2+Oqrr7Bu3Tp888030Ov1iImJwaVLl8w+f2xsLIKCggxfkZGRjuoKERHZATM3VFMaofCCAsXFxUhOTkZWVhZ+/PFHfPHFF9i2bZvZAEeqpKQE7dq1w/jx4/H222+bPKeoqAhFRUWG77OzsxEZGYmsrCwEBgbarR9ERFR9W06m4rGv4wEAJ94aBl9PRasmyAllZ2cjKCjIquu34q8eT09PtGzZEgDQvXt37N+/HwsWLMBnn31W5WM9PDwQFRWFpKQks+d4eXnBy8vLbu0lIiIi56b4sJQxvV4vy7RYotPpkJCQgAYNGji4VUREVFs4LEU1pWjmZsaMGRgxYgQaN26MnJwcrFy5EnFxcfjjjz8AABMmTECjRo0QGxsLAHjrrbfQp08ftGzZEpmZmZgzZw4uXLiASZMmKdkNIiKyI8Y2VFOKBjdpaWmYMGECrl69iqCgIHTu3Bl//PEHhgwZAgBITk6GVluRXLpx4wYmT56MlJQUhISEoHv37ti1a5dV9TlEROS8pEuWcW8pqinFC4prmy0FSUREVDv+OpWKR5eVFRQf/d9QBHp7KNwicja2XL+druaGiIiIqCYY3BARkVO5tcYTyBEY3BARkeI00jWKGdxQDTG4ISIi5cliG0Y3VDMMboiIyKlwWIpqisENERERqQqDGyIicipM3FBNMbghIiLFSUpuuIgf1RiDGyIicioMbaimGNwQEZHipAENEzdUUwxuiIiISFUY3BARkfKE9CZTN1QzDG6IiMi5MLahGmJwQ0REipNmaxjbUE0xuCEiIqfCgmKqKQY3RETkVFhzQzXF4IaIiBTHbA3ZE4MbIiJyKgx0qKYY3BARkeKEbCo4Uc0wuCEiIqfCvaWophjcEBGR4rj9AtkTgxsiIiJSFQY3RESkOA5FkT0xuCEiIqfCOIdqisENEREpTlZzw/lSVEMMboiIyKkMmBOH5bvPK90McmEMboiISHHGQ1Gz1h1XpiGkCgxuiIiISFUY3BARkRNgnQ3ZD4MbIiJySlNWHsTp1Bylm0EuiMENERE5pQ1Hr2LcZ7uVbga5IEWDm8WLF6Nz584IDAxEYGAgoqOj8fvvv1t8zOrVq9G2bVt4e3ujU6dO+O2332qptURE5Cjm1rbJzC+p3YaQKiga3ERERGD27Nk4cOAA4uPjcfvtt2PUqFE4ftx0lfyuXbswfvx4PPbYYzh06BBGjx6N0aNH49ixY7XcciIiInJWGuFka16HhoZizpw5eOyxxyrdd9999yEvLw8bNmwwHOvTpw+6du2KTz/91OTzFRUVoaioyPB9dnY2IiMjkZWVhcDAQPt3gIiIbPZbwlU89e1Bk/ednz2ylltDzig7OxtBQUFWXb+dpuZGp9Nh1apVyMvLQ3R0tMlzdu/ejcGDB8uODRs2DLt3mx+TjY2NRVBQkOErMjLSru0mIiLH2vHPNbyz4QSKS/VKN4VchLvSDUhISEB0dDQKCwvh7++PNWvWoH379ibPTUlJQf369WXH6tevj5SUFLPPP2PGDEyfPt3wfXnmhoiInIelMYQHv9wLAAgP8sak/s1rqUXkyhQPbtq0aYPDhw8jKysLP/74IyZOnIht27aZDXBs5eXlBS8vL7s8FxERKefSjQKlm0AuQvHgxtPTEy1btgQAdO/eHfv378eCBQvw2WefVTo3PDwcqampsmOpqakIDw+vlbYSEZFjcLNMsienqbkpp9frZQXAUtHR0diyZYvs2ObNm83W6BARkfNKySrE2E93Yf2RK6ynIbtSNHMzY8YMjBgxAo0bN0ZOTg5WrlyJuLg4/PHHHwCACRMmoFGjRoiNjQUATJ06FQMGDMDcuXMxcuRIrFq1CvHx8fj888+V7AYREVXD27+ewP7zN7D//A2rzneyyb3kxBQNbtLS0jBhwgRcvXoVQUFB6Ny5M/744w8MGTIEAJCcnAyttiK5FBMTg5UrV+K1117DzJkz0apVK6xduxYdO3ZUqgtERFRN2QVcoI8cQ9Hg5ssvv7R4f1xcXKVjY8eOxdixYx3UIiIiqi1ajcam8zU2nk+3LqeruSEioluDrbEKh6XIWgxuiIhIEbZmboisxeCGiIgUYWtow7wNWYvBDRERKYI1NOQoDG6IiEgRWsY25CAMboiISBG2FxQ7ph2kPgxuiIhIESwoJkdhcENERIpgbEOOwuCGiIgUwYJichQGN0REpAgOS5GjMLghIiJF2L7ODSuKyToMboiISBGcCk6OwuCGiIgUYeuwFKeCk7UY3BARkTKYuSEHYXBDRESKYEExOQqDGyIiUgQ3ziRHYXBDRESKYOaGHIXBDRERKcLW2IahEFmLwQ0RESnC1hWKOSxF1mJwQ0REiuA6N+QoDG6IiEgRtg5LcZ0bshaDGyIiUgQLislRGNwQEZEiGNyQozC4ISIiF8FxKbIOgxsiIlIEMzfkKAxuiIhIEZwtRY7C4IaIiBTBxA05CoMbIiJShK2L+BFZi8ENEREpguvckKMwuCEiIkWwoJgcRdHgJjY2Fj179kRAQADCwsIwevRoJCYmWnzMsmXLoNFoZF/e3t611GIiIrIXW0MbZm7IWooGN9u2bcPTTz+NPXv2YPPmzSgpKcHQoUORl5dn8XGBgYG4evWq4evChQu11GIiIrIXZm7IUdyV/OEbN26Ufb9s2TKEhYXhwIEDuO2228w+TqPRIDw83NHNIyIiB+JUcHIUp6q5ycrKAgCEhoZaPC83NxdNmjRBZGQkRo0ahePHj5s9t6ioCNnZ2bIvIiJyAjZmbgRXKCYrOU1wo9frMW3aNPTt2xcdO3Y0e16bNm3w1VdfYd26dfjmm2+g1+sRExODS5cumTw/NjYWQUFBhq/IyEhHdYGIiGzAzA05itMEN08//TSOHTuGVatWWTwvOjoaEyZMQNeuXTFgwAD8/PPPqFevHj777DOT58+YMQNZWVmGr4sXLzqi+UREZCPW3JCjKFpzU27KlCnYsGEDtm/fjoiICJse6+HhgaioKCQlJZm838vLC15eXvZoJhER2RFDG3IURTM3QghMmTIFa9aswV9//YVmzZrZ/Bw6nQ4JCQlo0KCBA1pIRESOwsQNOYqimZunn34aK1euxLp16xAQEICUlBQAQFBQEHx8fAAAEyZMQKNGjRAbGwsAeOutt9CnTx+0bNkSmZmZmDNnDi5cuIBJkyYp1g8iIrKdrdsvcJ0bspaiwc3ixYsBAAMHDpQdX7p0KR5++GEAQHJyMrTaigTTjRs3MHnyZKSkpCAkJATdu3fHrl270L59+9pqNhER2YFgtEIOomhwY80LOy4uTvb9vHnzMG/ePAe1iIiIagtjG3IUu9TcZGZm2uNpiIjoFmJrbMNYiKxlc3Dz/vvv4/vvvzd8P27cONSpUweNGjXCkSNH7No4IiIiIlvZHNx8+umnhoXwNm/ejM2bN+P333/HiBEj8OKLL9q9gUREpE4cliJHsbnmJiUlxRDcbNiwAePGjcPQoUPRtGlT9O7d2+4NJCIideJ2CuQoNmduQkJCDKv8bty4EYMHDwZQVhys0+ns2zoiIiIiG9mcuRkzZgzuv/9+tGrVChkZGRgxYgQA4NChQ2jZsqXdG0hEROpk67AUh7HIWjYHN/PmzUPTpk1x8eJFfPDBB/D39wcAXL16FU899ZTdG0hEROrEWIUcxebgxsPDAy+88EKl488995xdGkRERGQKa3TIWtVa52bFihXo168fGjZsiAsXLgAA5s+fj3Xr1tm1cUREpGIcZyIHsTm4Wbx4MaZPn44RI0YgMzPTUEQcHByM+fPn27t9RESkUgxtyFFsDm4++eQTLFmyBK+++irc3NwMx3v06IGEhAS7No6IiIjIVjYHN+fOnUNUVFSl415eXsjLy7NLo4iISP04KkWOYnNw06xZMxw+fLjS8Y0bN6Jdu3b2aBMREd0CbC4QZjBEVrJ5ttT06dPx9NNPo7CwEEII7Nu3D9999x1iY2PxxRdfOKKNRESkQszckKPYHNxMmjQJPj4+eO2115Cfn4/7778fDRs2xIIFC/Cf//zHEW0kIiIisprNwQ0APPDAA3jggQeQn5+P3NxchIWF2btdRESkcqYSN10jg3H4YqbV5xOZYnPNTUFBAfLz8wEAvr6+KCgowPz587Fp0ya7N46IiNTL1LCURlP77SD1sTm4GTVqFJYvXw4AyMzMRK9evTB37lyMGjUKixcvtnsDiYjo1mEpthEs0iEr2RzcHDx4EP379wcA/PjjjwgPD8eFCxewfPlyfPzxx3ZvIBERqZOp2VIapm7IDmwObvLz8xEQEAAA2LRpE8aMGQOtVos+ffoYtmIgIiKqkolEjJaxDdmBzcFNy5YtsXbtWly8eBF//PEHhg4dCgBIS0tDYGCg3RtIRES3Do3FgSki69gc3MyaNQsvvPACmjZtit69eyM6OhpAWRbH1MrFREREppisoGFsQ3Zg81Twe++9F/369cPVq1fRpUsXw/E77rgD//rXv+zaOCIiUi9TBcKMbcgeqrXOTXh4OMLDw2XHevXqZZcGERHRrUtroaCYc6XIWtUKbuLj4/HDDz8gOTkZxcXFsvt+/vlnuzSMiIjUjevckKPYXHOzatUqxMTE4OTJk1izZg1KSkpw/Phx/PXXXwgKCnJEG4mISIXKY5umdXwNxywFN1zmhqxlc3Dz3nvvYd68efjll1/g6emJBQsW4NSpUxg3bhwaN27siDYSEZEKlQcr0rVtOFuK7MHm4ObMmTMYOXIkAMDT0xN5eXnQaDR47rnn8Pnnn9u9gUREpG7SbA2HpcgebA5uQkJCkJOTAwBo1KgRjh07BqBsK4byPaeIiIiqUr5CsTSesbRC8fojV7gFA1nF5uDmtttuw+bNmwEAY8eOxdSpUzF58mSMHz8ed9xxh90bSERE6lQep2hlw1KW7TqT4bgGkWrYPFtq4cKFKCwsBAC8+uqr8PDwwK5du/Dvf/8br732mt0bSERE6iYLbqqIbi5e5wgBVc3mzE1oaCgaNmxY9mCtFq+88grWr1+PuXPnIiQkxKbnio2NRc+ePREQEICwsDCMHj0aiYmJVT5u9erVaNu2Lby9vdGpUyf89ttvtnaDiIichKzmpopzOShF1rA6uLly5QpeeOEFZGdnV7ovKysLL774IlJTU2364du2bcPTTz+NPXv2YPPmzSgpKcHQoUORl5dn9jG7du3C+PHj8dhjj+HQoUMYPXo0Ro8ebaj9ISIi12ByheIqUjcsuSFrWB3cfPTRR8jOzja5OWZQUBBycnLw0Ucf2fTDN27ciIcffhgdOnRAly5dsGzZMiQnJ+PAgQNmH7NgwQIMHz4cL774Itq1a4e3334b3bp1w8KFC2362URE5BzM1dyY2iFcMHdDVrA6uNm4cSMmTJhg9v4JEyZgw4YNNWpMVlYWgLKhL3N2796NwYMHy44NGzYMu3fvNnl+UVERsrOzZV9ERKS88jBFK7kSyda84bxwqiarg5tz585ZXKQvIiIC58+fr3ZD9Ho9pk2bhr59+6Jjx45mz0tJSUH9+vVlx+rXr4+UlBST58fGxiIoKMjwFRkZWe02EhGR/RgW8YPpgmJToQ2HpcgaVgc3Pj4+FoOX8+fPw8fHp9oNefrpp3Hs2DGsWrWq2s9hyowZM5CVlWX4unjxol2fn4iIasZcQKPRcFE/qh6rg5vevXtjxYoVZu9fvnx5tXcGnzJlCjZs2ICtW7ciIiLC4rnh4eGVCpdTU1Mr7VJezsvLC4GBgbIvIiJSnmERPzNTwTUmNmNg4oasYXVw88ILL2Dp0qV44YUXZMFFamoqnn/+eSxbtgwvvPCCTT9cCIEpU6ZgzZo1+Ouvv9CsWbMqHxMdHY0tW7bIjm3evBnR0dE2/WwiIlJWxbBUBW1VqRqOS5EVrF7Eb9CgQVi0aBGmTp2KefPmITAwEBqNBllZWfDw8MAnn3yC22+/3aYf/vTTT2PlypVYt24dAgICDHUzQUFBhiGuCRMmoFGjRoiNjQUATJ06FQMGDMDcuXMxcuRIrFq1CvHx8dzXiojIxRgKim3YW4qhDVnDphWKn3jiCdx111344YcfkJSUBCEEWrdujXvvvbfK4SRTFi9eDAAYOHCg7PjSpUvx8MMPAwCSk5OhlZTSx8TEYOXKlXjttdcwc+ZMtGrVCmvXrrVYhExERM5Ly13Byc5s3n6hUaNGeO655+zyw63ZAC0uLq7SsbFjx2Ls2LF2aQMRESnDMCwlqyI2c9voMUSW2Lz9AhERkX2U7wpufuNM47VuuCs4WYPBDRERKUoav1RVUMzQhqzB4IaIiBRhaliKk6XIHhjcEBGRIsoDFXN7S5lcodihLSK1sDm4mTVrFrZu3YrCwkJHtIeIiG4xWjP7SXF1Yqoum4Ob3bt34+6770ZwcDD69++P1157DX/++ScKCgoc0T4iIlKpihWKK44ZBzTG8c3plBzMXJOAlCx+wCbzbA5uNm/ejMzMTGzZsgV33nkn4uPjMWbMGAQHB6Nfv36OaCMREalQRc2N9evcfB9/ESv3JuPZ7w45smnk4mxe5wYA3N3d0bdvX9SrVw+hoaEICAjA2rVrcerUKXu3j4iIVM54s8yK4xqYq7I5cTXboW0i12Zz5ubzzz/H/fffj0aNGiEmJgYbN25Ev379EB8fj/T0dEe0kYiIVMjk9guKtITUxubMzZNPPol69erh+eefx1NPPQV/f39HtIuIiFTO1LBUlRtnGh7LeVNkns2Zm59//hkPPPAAVq1ahXr16iEmJgYzZ87Epk2bkJ+f74g2EhGRCpUXFFvaOJMzpqg6bM7cjB49GqNHjwYAZGVl4e+//8bq1atx1113QavVcoo4ERHZyPT0bwY2VF3VKijOyMjAtm3bEBcXh7i4OBw/fhwhISHo37+/vdtHRERqZWrjTCurbjgoRZbYHNx06tQJJ0+eREhICG677TZMnjwZAwYMQOfOnR3RPiIiUimTBcXM1pAdVKugeMCAAejYsaMj2kNERLcYc9svWMJ6YrLE5uDm6aefBgAUFxfj3LlzaNGiBdzdqzW6RUREt7DyGU/m6mw0sLzWDZE5Ns+WKigowGOPPQZfX1906NABycnJAIBnnnkGs2fPtnsDiYhIfVKzC5FXrANQvangRJbYHNy88sorOHLkCOLi4uDt7W04PnjwYHz//fd2bRwREalPSlYher+3BZtPpAIwvxO4xkKgI5jNIQtsHk9au3Ytvv/+e/Tp00f2wuvQoQPOnDlj18YREZH67D2XIfteY2ZXcEtYc0OW2Jy5SU9PR1hYWKXjeXl5Vr8oiYjo1uXj4Sb7Xmvm0mHpisLYhiyxObjp0aMHfv31V8P35QHNF198gejoaPu1jIiIVMm7UnBjoeaGn5mpGmwelnrvvfcwYsQInDhxAqWlpViwYAFOnDiBXbt2Ydu2bY5oIxERqZi5XcGJqsvmzE2/fv1w+PBhlJaWolOnTti0aRPCwsKwe/dudO/e3RFtJCIiFSnR6WXfa6qxzg3HpciSai1Q06JFCyxZssTebSEioltA5eDG9G0OSVF12Zy5ISIiqokSnTztYq6g2BJOBSdLrM7caLXaKmdDaTQalJaW1rhRRESkXpUyNzA/FZzJG6oOq4ObNWvWmL1v9+7d+Pjjj6HX682eQ0REBFQObrQcQyA7szq4GTVqVKVjiYmJeOWVV/DLL7/ggQcewFtvvWXXxhERkfoUGw1LmSsotrjODUelyIJqxctXrlzB5MmT0alTJ5SWluLw4cP4+uuv0aRJE3u3j4iIVKak1HhYisi+bApusrKy8PLLL6Nly5Y4fvw4tmzZgl9++QUdO3Z0VPuIiEhlLM2WshYTN2SJ1cNSH3zwAd5//32Eh4fju+++MzlMRUREVJVKNTeyueCSmxoNF/WjarE6uHnllVfg4+ODli1b4uuvv8bXX39t8ryff/7Z6h++fft2zJkzBwcOHMDVq1exZs0ajB492uz5cXFxGDRoUKXjV69eRXh4uNU/l4iIlFOp5sbMeQxsqLqsDm4mTJhg940x8/Ly0KVLFzz66KMYM2aM1Y9LTExEYGCg4XtTG3kSEZFzsrxCsbW7gnNgisyzOrhZtmyZ3X/4iBEjMGLECJsfFxYWhuDgYLu3h4iIHK+oxMKwFJEduOTqAl27dkWDBg0wZMgQ7Ny50+K5RUVFyM7Oln0REZEyNh5LwVc7z8mOmd1+wQLmbcgSlwpuGjRogE8//RQ//fQTfvrpJ0RGRmLgwIE4ePCg2cfExsYiKCjI8BUZGVmLLSYiIqknvzlQ6ZjZmhtYP0xFJFWtjTOV0qZNG7Rp08bwfUxMDM6cOYN58+ZhxYoVJh8zY8YMTJ8+3fB9dnY2AxwiIiei1VZjV3AiC1wquDGlV69e2LFjh9n7vby84OXlVYstIiIiW1QnoGE9MVniUsNSphw+fBgNGjRQuhlERFRNstlSTN2QHSiaucnNzUVSUpLh+3PnzuHw4cMIDQ1F48aNMWPGDFy+fBnLly8HAMyfPx/NmjVDhw4dUFhYiC+++AJ//fUXNm3apFQXiIiohrRmAhp7Lz9Ctw5Fg5v4+HjZonzltTETJ07EsmXLcPXqVSQnJxvuLy4uxvPPP4/Lly/D19cXnTt3xp9//mlyYT8iInIN8gWKGdBQzSka3AwcONDiQkzGa+u89NJLeOmllxzcKiIiqk2WAhomb6g6XL7mhoiIXJvZYSmj7+sHcnIIWYfBDRER1Rp3E5GMuYJijUYe4AT5eDiwZaQmDG6IiKjWuJkMbsyfLy1c4DYNZC0GN0REVGtMZW60Gi7iR/bF4IaIiGqNycyN2bPlpcacGk7WYnBDRES1psphKQsBDEMbshaDGyIiqjVu2sqXHUsZGel9Jh5KZBJfKkREVGtsrbmRroXGBf7IWgxuiIio1tgyW8r4OEtuyFoMboiIqNZUVVBcOaDhTCqyHYMbIiKqNaaCG2vXr+FsKbIWgxsiIqo1psITSxtnWjmRikiGwQ0REdUaU1slm8vIaIzOZ2xD1mJwQ0REtUYvKoc35jbONMZhKbIWgxsiIqo1On3l4MZiQbHktrVBEBGDGyIiqjVN6vhWOubmVnEpkmZ2jLM8XOeGrMXghoiIao2fp3ulY16S4KZUVxHQFJfqjdI6jmwZqQmDGyIiqjXloUuAV0WQ4+lecSkq0ekNt0uNhrA4LEXWYnBDRES1pnw7Ba0kUvFwkwY3QnK7ItABOCxF1mNwQ0REtaa8jEa6mJ80IyMNaKSBDsB1bsh6DG6IiKjWlBcJyzbLlAQtpXq98UNMnkdkCYMbIiKqNeW5GHn9TMU3xaWmlvkrfwyjG7IOgxsiIqo1poalpIwzNwxnqDoY3BARUa2paljKuIhYiisUk7UY3BARUa2TZm6kIYtxEbEUQxuyFoMbIiKqNeWZG3PDUpYyN8YPOZOea7d2kbowuCEiolpTXnMjDVSkw02lljI3RsNSr605Zte2kXowuCEiolpjKnMjDVmKjRfu05g+DwAKSnT2bh6pBIMbIiKqNRWZGzOzpXR6eLiZvs/4IcYbaxKVY3BDRES1pmKdG3OL+AnZdgxSxsNSloaw6NbG4IaIiGqNMDUsJYlZikv1cDdTbGx8lJkbMkfR4Gb79u24++670bBhQ2g0Gqxdu7bKx8TFxaFbt27w8vJCy5YtsWzZMoe3k4iI7MMwLGV2ET8h2yVcynhYSqdncEOmKRrc5OXloUuXLli0aJFV5587dw4jR47EoEGDcPjwYUybNg2TJk3CH3/84eCWEhGRPRgKiqWzpSQ5mRKdHgPbhAEAwgK8ZAGN8a7gDG7IHHclf/iIESMwYsQIq8//9NNP0axZM8ydOxcA0K5dO+zYsQPz5s3DsGHDHNVMIiKyE1M1N9KYpVQn8L97OqBteACGdwzHXZ/sMNynNfo4ruOwFJnhUjU3u3fvxuDBg2XHhg0bht27d5t9TFFREbKzs2VfRESkDFPDUhoAnjeLiDs1CoK/lzsm9W+OiBBf2WONMzcXMvJRXGp+0T+6dblUcJOSkoL69evLjtWvXx/Z2dkoKCgw+ZjY2FgEBQUZviIjI2ujqUREZIKhoNiogOb3af3x34Et8N6YTuYfbKJM58cDl+zZPFIJlwpuqmPGjBnIysoyfF28eFHpJhER3bLKB5Lks6U0aFHPHy8Pb4tQP0+zjzVVgpyRW2TfBpIqKFpzY6vw8HCkpqbKjqWmpiIwMBA+Pj4mH+Pl5QUvL6/aaB4REVXBsCu4mRWKjUnvM7XwX1gg39+pMpfK3ERHR2PLli2yY5s3b0Z0dLRCLSIiIluY2lvKWqYWNX75pwT8/U96zRpFqqNocJObm4vDhw/j8OHDAMqmeh8+fBjJyckAyoaUJkyYYDj/ySefxNmzZ/HSSy/h1KlT+L//+z/88MMPeO6555RoPhER2ah89rabmRWKLTF32kNf7qtZo0h1FA1u4uPjERUVhaioKADA9OnTERUVhVmzZgEArl69agh0AKBZs2b49ddfsXnzZnTp0gVz587FF198wWngREQuQpgcljIf3ci3aahGuoduSYrW3AwcONDwQjfF1OrDAwcOxKFDhxzYKiIicjRrMzfubrZneIhcquaGiIhcm97E3lLm9pIqu6/iMmUpw0MkxeCG6BZy6UY++r3/F5ZsP6t0U+gWVZ6sl2ZhpNkZYx7M3FA1MLghuoW8vzERl24U4N3fTirdFLpFmc7cmL8UebhJMzdE1mFwQ3QLuZ7HBc9IWYZF/CRpGEuZG3dJcGNqnRsiUxjcEN1C8ot1SjeBbnUm9paynLnhsBTZjsEN0S0kv4jBDdW+dYcv477PduNablHFsJS1mRstgxuynUttv0BENZNfUqp0E+gWNHXVYQDA7N9PGYalpMkai7OlpDU3jG7ISszcEN1CCiTDUjq9+TWmiBxhV9I1FJaUvQa1ssyN+UuRp5mC4pZh/nZvH6kHMzdEt4Bjl7Pg7qaR1dwUlOjg78W3AKo9V7IKDbetXufGTM2NpccQ8Z2NSOVyi0px1yc7Kh3PLyplcEOKkYYm1VnEjzOnyBIOSxGp3I28YpPH8zhzihQkHRS1draUNAZyY+aGLGBwQ+TClu48h9XxFy2eozVzEcgrYnExKUcv2VfQ2nVupAXF5l7XRACHpYhc1pXMArz5ywkAwJhuEWY/yerNFA5zzRtSknTPZEtZGA8z9zG2IUuYuSFyUTmFFZmXvGLzWZhind7kcUuPIXI0acztYWG2lLmCYjfW3JAFDG6IXFSJJGj562Qa+ry3Bfcu3oWiUnlGplRnJnNTpMPp1BxczSpwaDuJTBGS1I2lLIyHme0XjIelvt+fbL/GkctjcEPkonIlNTOvrT2GlOxCxF+4gVNXc2TnlZjJ3JxKycbQedsxfP7fDm0nkSnSYSlLi/OZ2zjTOHPz8k8J9moaqQCDGyIXJS0IlgY60lqaEp0e3+83XXD888HLAICsghLZp2hSnl4vVP870VvZP3PbL3C2FFnC4IbIhZTq9Pjwj0TsOnNNFtBIFUi2WPh+/0Ws2HNBdn/5xeJyZsVwFIuLnUdxqR7D5m/HpK/jlW6KQ1m7QDZnS1F1MLghciHf7b+IhVuTcP+SvWaDmw1Hr2LEgr9x4ko2dp/NqHR//UDvSsekxclU+7IKSvDQl3vx04FLOHDhBv5Jy8WWU2kAyrbMyMgtUriF9idgXXTTv1Vdw215QbG9W0RqwuCGyIUcuZhpuP3qmmMmz/n54GWcvJqNKd8dRD1/r0r3hweZCm5K7NZGst0nW/7B3/9cw/Orj1Sqker93p/o/s6fuG5mMUZXZe2oW9+WdbFyUm/snnG7bIViDkuRJQxuiFxIrg0Zlms5RcgqqBy0hJvI3GQzc6MoaeBSqq8Ibkp0esPvRhrYqoG1NTcAENOyLhoE+cgyN9x+gSxhcEPkQnKKbMuwZOZX/rTfKMSn8vMyc6MoneRCXyKZut/mtd8Nt//77QG8se4YsvJLMHNNAuLPX6/VNtpbdTal5/YLZC0GN0Qu5ExantXnajQak5mb6OZ1Kh1jzY2ySiVXeum6RNIAoLBEj693X8Dsjaewcm8y7v10d2020e6qMxtMugcVC4rJEgY3RC7iWm4RUrILZccs7aYMAJkmgps+kuCmS0QQAAY3Sjh5NRtjP92FvWczZFtkmFuXqNyxy1mOblqtqM5Md+kmmlyhmCxhcEPkIi5kVM7amJr5JJWVXzm48fF0w2/P9scPT0SjRZg/ACCbw1K1buJX+7D//A3c9/keWeamquAmQSXBTXW4STI3HJYiSxjcEDkhIUSl6b/pOZWnA5ua+SRlnLn5d7cIAED7hoHo1SwUgd4eAFhzo4Q0ye9Tmrl58cejSjSn1k2+rTnq+nvh8duaW/0YaebGVEHx6dScSsfo1sTghsgJzd10Gt3f+RNbE9MMx6oKbhoaBTq5RaXQGVVtzh3XRfZ9gLc7AA5LKU2nwtWI03OKkJSWa/b+sAAv7Jt5B2be2c7q55QOw5raa/PJFQdsaiOpF4MbIie0cGsSAODtDSfw1LcHMP/P07h0o/IGl9Jp3ff3biy7zziwMaU8uFkdfwm93v0Ti+PO1KTZVE2ZJoYPXV3Pd//E4I+24XJmgckhJI3G9qJgNzfpsFTly9fZa3l4/ocjtjeWVIfBDZGTkda/nE3Pw28JKZj/5z/4bPvZSuc2kGRrujcJtflnBdwclioo0SEtpwjvbzyFSzfyq9FqqonD1VzD5uL1fKfag2rtocvYfjpddizhUiZ8PdwAAHUli0pWZ52aqjI3APDTwUs2Py+pD4MbIichhECJTo+L1y0HF9K6gzr+nobbXSODbf6Z5ZkbqdRs9S317yw2HL2Cuz/ZgeQM+wSQ/T/Yis9NBL1KuJCRh2nfH8aEr/bJjpfohGHYTfrarU45sDQDxNlSZIlTBDeLFi1C06ZN4e3tjd69e2Pfvn1mz122bBk0Go3sy9vbclElkSt4+aej6PHOn0i4ZP1smK6RIYbbPp5u+PTB7nhtpOkahvG9Glc6Vl5QLKXGfYycxZSVh5BwOQuvrk2w23PG/n7Kbs9VE9dyKxaMlGaTnvnukGFjVg9puqUasYk0c8N1bsiSyh/batn333+P6dOn49NPP0Xv3r0xf/58DBs2DImJiQgLCzP5mMDAQCQmJhq+1zCCJxX4Ib4snT7vz9NWP6ZZXT/89N9oQ7p/eMdwFJbo8M6vJw3nDGpTD1Nub4XON9e0kTKVuVHbHkbOyNymp9XhLNd4aVal1Ey9l3sVs51s+RnM3JAlimduPvroI0yePBmPPPII2rdvj08//RS+vr746quvzD5Go9EgPDzc8FW/fv1abDGR/UmLf6saFnr29lYAgKHty1733ZuEokkdP8P9nkbFCCG+nujeJET+qfmmAFOZGwY3DmfPC7PPzXoWJej1AuuPXEFyRr6sT+bW6vGQFAFX53+AKxSTtRQNboqLi3HgwAEMHjzYcEyr1WLw4MHYvdv80uK5ublo0qQJIiMjMWrUKBw/ftzsuUVFRcjOzpZ9ETkbW7IlwzqG4++XBmHRA91M3m/8pi/9tGws0ETm5lpuEYQQsrVXyL7suQBdXrEOf55Itdvz2WLt4ct49rtDuG3OVlmfikpMBzfS12J1Mu7Sn8GNM8kSRYOba9euQafTVcq81K9fHykpKSYf06ZNG3z11VdYt24dvvnmG+j1esTExODSJdMV8rGxsQgKCjJ8RUZG2r0fRDVlag0bcwK9PRAZ6msyE2OKpYuAqcxNVkEJ3vn1JDq/uanK4maqHnuvrjtpebxdn89a+8/fMHm8sFRn8rj0pWgp6DbHzYrZUkSAEwxL2So6OhoTJkxA165dMWDAAPz888+oV68ePvvsM5Pnz5gxA1lZWYavixcv1nKLiaqWmiPfMyrEt3LQUS7Qx7ZSOUvpe2+Pym8BBy/cwJc7ziG3qBS/H7tq088i6zhq64Cz6bk4ebX2stPS2U96SRGxuUykdNa68fCpNdytrLmxZo0nUjdFg5u6devCzc0NqanylGpqairCw8Oteg4PDw9ERUUhKSnJ5P1eXl4IDAyUfRE5m7Pp8n2jejY1vWaNu1Zjc42FpeuoqaGB85Jpyu4mFkqjmnNEcJOVX4Lb527DiAV/m9xTzBGk2cNxn1WUEoz8eIdNj7WWm5nZUtL1c4Cq9+dSwtn0XJP7w5FjKPrO5enpie7du2PLli2GY3q9Hlu2bEF0dLRVz6HT6ZCQkIAGDRo4qplEDpeUJt8Tp1k9P5PnBft62FyrMLS9dR8UTDG1qzjV3NXMwqpPuql5XdOvBWNd3tpkuH05s/Jq1vZy7HIW7lm4A7uSrsmGlsqne1sizdxUJ8Azl7mpK1nvCXC+4Ca/uBS3z92GAXPiUOpkbVMrxT+WTZ8+HUuWLMHXX3+NkydP4r///S/y8vLwyCOPAAAmTJiAGTNmGM5/6623sGnTJpw9exYHDx7Egw8+iAsXLmDSpElKdYGoxo5fkQ8lhAWYXrvJx9P2mTG3ta5n1Xlt6gdUOpaZz5lTjpBowwaPvl62/86LHXgBnfR1PI5eysL9X+yVzX6yRk0Hi7RmMjfGWaBSnXMNS2VI1gAqKKk6CKSaU3ydm/vuuw/p6emYNWsWUlJS0LVrV2zcuNFQZJycnAyt5A/oxo0bmDx5MlJSUhASEoLu3btj165daN++vVJdIKqRzPxiJFyWL9xX198TXSODKy3LX2DFp2Mpa1Yt/t/d7bH6wCW8emc73P/FXqO2MXNTU0IIFJXq4V3NKdvVqU1xZObiuiTgrU5RcE3It1+ouG3cDmfL3EgVluhh5rML2ZHimRsAmDJlCi5cuICioiLs3bsXvXv3NtwXFxeHZcuWGb6fN2+e4dyUlBT8+uuviIqKUqDVRPaRmJIDIeS7enu4afHNpN5YOam37FzpKrDWsCbz/3DfZvj12f5oKhn+aBtelsW5wcxNjU1eHo8ub26yabp/qzB/w21Pd9Nv05aGq0p0esQlpuGtX07Y5UL/44FLGDhnK5LScmQBRnXqZmrC3CJ+xhmkDzclwplIfweFzNzUCqcIbohuVT/sv4i5m8tWJI4M9TUcbxseAH8vd/RsJi8sLl+4z1q2rAUSLJmh9Z+eZUsmZLHmpsb+PJmGolI9fjlyxerHSLM85gIIc0EPACzdeR4PL92Pr3aewzd7LljfWDNeWH0E5zPy8eKPR+UZExvrZmq6yae5Rfw83OXtKF/t21kUldovuEnLLsSkr/cjLjGtps1SNcWHpYhuVVkFJXjpp6OG78ODvPHn9NuQllOE5vXKPrlLLx4dGwXivTGdbPoZttQe+3q649MHuwMA6gWUFWgm39x1+lpuMeoFeFl6OFXB0vTktuEBOJVSUYcjjRm8zAQx5o4DwGbJon7nr9Vsho60ADansFQWbLnXcuZGmqCRjkQ5+6y+YklwU9Oam3d/O4k/T6bhz5NpOD97pOy+K5kFWLk3GQ9FN0H9QPNjX3P+OIXreSV4718dVbt9EYMbIoWcSc+VfR8e6I2WYQFoGVZR2Ct94/nvgJaVprxWRWPjIvfDO4bL2paZX4JmM34DACz4T1eM6trIpuejCnoLWYs6RrN9pMxlaLzcravhqUlxsU4vMPijbYbvS3V6WeamppkYW0mDGDfZ8JhzX6ClmRtb6+aMZVgYmh6/ZA8uZOTjn7QcPHN7Kxy5lIn7ezWWvY8UluiwaOsZAMATtzVHkzq+KNbprX49uQrnDneJVOxMmjy4sfRJq7raNqg8A8oawT6VFxHcmXStps255UizHtLNTAGgSZ2KYUg3C5kHcwXFloalpL7bd7Fai9rp9QIXr+fL1j0q0QlZNtFSwGZKTWMhc+vcuFLmprDUfLB5NasAr689hrNGH3ykLBWmX7j5uzqUnIm7PtmBV9ccQ8tXf8fesxm4Z+EOvLY2ASlZFcsQFJXq8eraY+j21mZcuqGu1cid+xVBpGJnjBbuMxfclH/oimocbPVzr326Lx7t2wwvDmtTrbYFmQhu8mr4ifNWZOlC5i35pFypdkVTddGupWEpY499vd+q8y5nFhgufhOX7sPAD+Mq3X9VcnHcmZRhdRsAQFfD6MZXshSCtJ7M1KytjU60unaRZDsK48xNblEp9p27Dr1eYPzne7BizwW8uuaY2efyNVoOIruwBAeTb8j2gkuTbOei0wvc9/keHL2UhW/2JOO/3x403JdTWIKVe5ORV6zDku1nq90/Z8RhKSKFVBqWCjI95HT49aHIKihBw2Afq5+7a2SwVdPAzTFVS2HL/ldUxtIQhJdk6wtLhd/mMjTST/CNgn0sLtwXl5iOtJxCs+snAWXDFX1n/wUASHp3BP7+p+pM3bbT6VWeI1XTzVgbBvtgcv9m8PFwk2W0TAWAT35zEM/c3hITY5raPJxrL5czC/DQl3tRX/L/nm1UpP/wV/sQf+EGYsd0MmTJTltYB8nPaN2jexfvwunUXHwy3rpZw9LtOXIKSw23i51sbaCaYuaGSCHWDksF+XqgsWQIo7YF3Nw5/FougxtbWZoZYylzI/1OeuGWBjrS26b2CDNWbCGLBMjXNMorsk+Wro6fJ+r4VdQT1TRzAwCvjmyP6UPbyIqLzdXcfPJXEqauOlTjn1ld7/56AmfT87D7bEWGq3wSQXm9UvyFss1Hv99fse9hRKgvsgtLMHl5fKVZdj4eFTmJ/4tLwunUsveRZ76zvZ/ZhRW/c2deG6g6GNwQKSA9pwjnjPaZsfSpWgkrJ/fGy8PbYs1TMQCYuakO4+BGmkGQZm7cLBTESoMYLzO3rVkgcGtiOqb/cFh2QZOSxlfmdvU2R7rfWfsGFfv3abUaWW2MPTe0lA9Lmb+U2Tp0Zi9CCPyWkGLyvpd+PIK+s/+S/S7cZf9Pesz9IxGbT6Time8OQacXeGJFPN799YQskP1gY83W81m5N9lwu7hUj6tZBViy/azZ14gr4bAUkQJ2nbkGIcqmAIf6eaJRsI/VBaK1JaZFXcS0qGvYhDGnsBR//5OO06m5mBjdpNanAbsi42m/Pp7S4MRCzY2E9D4vdzfkoGwoQfp6kQYXWg1gKoZ4fW1ZHUfr+gGYEN0Evp7yt/9SyYOs2SdKqmldP8Nwh7RdQgDSl4k9J1dJg5vqrOLsaH+eNL8OTfk6PI8uraiFkhZnl+oEjlyqWLV877kM/HG8bHr/kwNa2K2Ne89dN9wuLtXj4a/2IzE1B/+k5eCDe7tI2qPH7N9PoXfzOhhi41pbSnG+VwSRyv1y5AqmrjoMAOgSEYyVk/tgztgulh+koEAfd8PF46Ev9+HtDSewbNd5ZRvlIoxrbqRDUdJP4JY2kZQHN1VnbhqH+lpc32j276fQftYf+HjLP7Lj0v2Y8otLjR9mkbQvXrLgRsiCEHtmbmqymGBtsFQ3U658SAoADiZnGm6fSsmRbb2y92xFEFJg4+/GWueu5Rn2PPvdKOO07vAVfLHjHCYvj3fIz3YEBjdEtWj76XTZ2HiDYOcaijJFo9FUWsBvk2SROKos4VIWjl/JqpS5kQYh0syNm1E0Is1+SId15EGENFCSPJdWY9WGlh9tPo3iUj0eW7Yfnd74A3skdSG7z9g2lCPNnHhJ2iIgz7DYo+amnJ9XReapqiziL0eu4PW1x+waXFWlunuJmbJAEog6atVw2WaumrIFAZ9YEY89ZzMqFavnFpUir8gxQZa9MLghqkU7jNaKkRZbOrO6RovMXb5hfmbOrS6nsAR3L9yBkR/vkM1GAeTDR9JAxXgqs5+n6SGrQMkUfdmwlOR8jUYDa9duXL77PLacSkNOUalstWzjNXmq4iUL2irapRdClmGp6WwpKX9JcCMtKG4p2Zer3DPfHcKKPRew9tBlu/38qjhqYcHa2BIlp7AUr609hj+Op+I/n++R3VdUqkNM7BYM+jCuVoNFWzG4IapFxqnqIF/XCG5CjYKwK1kFsrU7qIJ0jRHjGSzSImLpJ3vjJfB9JRduaYYiRPJ6kQ1LGQ0FSZ+tqYWZdrYGMeZIf740i6PXy4Mbe07JlgY30kX8/DzNZ0zSHFwUL4TA+Wt50OmFzXVL1rqRXzvFvn+dqqgZkr6ejl/JRnZhKdJyipCR57yTDBjcEDnY1awC3L9kD/44noIDkjH2+oFeGHFzuwNn104yA8bbQwshmL0xp6jE/JRaHzMZDuPP+L6S8+pIAoKGkmFMczU3Qsj3FDMuHK6J5vVM70QuzdxIh9EE5G1Z9EAUejUNxbdGu91Xh7+3dFhKUpdkYTjI1hWVrVWq0+PSjXysO3wFAz+MQ4uZv8mG+exJWotTFXuVIkkDquNXKtbJSc7Ix+Tl8Vhvw6awtYWzpYgcbOFfSdh1JgO7JHUM3z/eB72ahbrMpnVPDmyBhMtZ6NU0FD8fuoxz1/KQLtng05ysghKTqx1bUqLTY84fibiSWYB593U1u0Kvs8qzUPDp7WG6TkajKQtWyvcgkq5CW1eSNWtdv2I7DQ9JcGP8fyTdU0y+qq/pmVTWMl4dt5w0cyMdjRECCPCu+P23DAvAD09GV78BEtLMjawtFoIbRw2jPPnNQfx5Ul6HFpcoX+DQXauRzUirDb6e7si9WRvjptVUu/9f7TxnuH1MMotrxZ4L2HwiFZtPpGJgm3oI9Lbtb92RXOtdg255Or3AjJ8T8L/1xyGEwLbT6Vi0NcmuY/n2diFDvmdL/1Z10bt5HZcJbAAg0NsDKx7rjWfuaIV6NzMJaTlFWPDnP9h0PAVbE9Pw78W7DL8XoGxn6i5vbqo0K6dcblEpfoi/iNOpOUhKy8Gkr+Nx8mo2Pt9+Fp9vP4sNR6/itg+2oukrv6L/B3+5zN43xivQSuuVpHU20otzbmGpYbFEQD4sJc3ctJQEk9JaHOnsprIi3oqfLx0KC7Qx0DTmYyZwkK3Zo5UPkUU3r1Ojn2mOdPhJujGlt4UlFRwV3BgHNqZ4uGktzopzBGmgZy4wtdWGoxVZmut5FZt4HrucZep0xTBzQy6hRKeHVqPBR5sT8d2+soWnmtTxxZu/nABQtt1Am/AAZOYXo0U9f8UDh1KdHkcvZ6FLRDCOX5H/0fdx0Jt9bakbUHaxXrbrvGGYzdNNi2KdHgcu3MDDMU3RtK4fZvycAKBsVs6zd7SSPceNvGJEvb0ZQNmbbr0AL1zIyMeWU6mytVDK9zG6eL0Am46n4tF+zRzdvRozLiL283LHtZs7OfuYydxculEAf8l50gu3dMfwMMkq1tILuvRnCiFkr39pDUyAt7thJWJPd22VqxYbM5cVkc38ksQWegE8NagF/jqVavfXvXSGlLT+y1LmxlHDUtZwd9NALyqyJ+V/M44kDWh8Pd1krxM/TzfDfnG2ZJWke8xdkgxNO9sinwxuyOmlZBViyLxt6BIRLBvHLg9sACAuMQ1PrjiAnKJSLJnQQ/GFpl5dcwzfx1/E04NaVCoAHB3VSKFW2Ud55kZaPyR9k160NQnFOr3F7RqOSj7l5RfrDNktS9eeq1kFKNHp4aaRr3rrbHKMVnf1k9S8SC+80jqRSzcKMLxjOJbtOg8fDzdZEBPi6wk/TzcUlOgQEVKxv5h0i4QcybRcvZDX8EhnVZUNGxTcvF0RTPl6ullVAGsucyPNSEmHxAQEAr09sOm5AVU+d01IM7eWtqL45K8kPDe4tSKvHw+3slq18qDUz8sNxfllt2s6XGguOJEGN2Wvw4q/SR9Pd0Og4uvphuxC26d2n7tWscp6ek4Rtp9Ox9e7zuOdf3VEgyDr98JzBA5LkVMTQmDIR9uQU1iKHUnXzH66WPL3OcMb/O4zGSjR6bHu8OVamTZprKhUh+/jy/aJWbT1TKX7G9mwAaYzCjOzB1a51QcuYd1heYHh6EU7cSY9F2nZhdh2Oh3JRltPWFJ+Qf3lyFV0eXMTer77Z6VsmDMxfs1JNzqUrhfkrtWgT/NQAMDIzg3w8vC2eGVEW/z6bD/8K6oRRnZugLdGdYCbVoPdM+/AwdeHyIKjLpFBhtvSgEpAHt14GGVuKm5XDFFJh8jahlfU9RjzMTO0Ic3cSLMjjh4tfqxfM7QND8BdnRsajlW1vsy+89ct3m8ra3cfd9NqZAGttCDaXP1QgJnjxqTPJSX9fRn/7qQF6fYoOk/PKcKEr/Zhy6k0vLn+RNUPcDBmbsgpFRTrsGLPeTQM9pF9KgWAEF8Pi9Mh/0nLwfu/n8IXO87hni4NMXdcF+w7dx0tw/zNbk5pD7vPZGD7P+mymUXl6vp74lpuMe7tHuGwn19bWplYRwQAwgO9kZJdaPK+wxcz8b/1x3Hgwg2bp8h2jgjC3nPXDc+dX6zDsp3n8eaoDjidmot2DQJkF1cl6PUCi7YmoXvTEGw/LV/LSDqVW7oGy/W8Ynw+oQe2JaZjcLv68PF0ky2tv+j+bobb0kLNHS8Pwpn0PMS0qGs4Jh1uyCksNZu5MRfQ+Hu7G6ZJ+xldUKWFqNZkbqQL9QkHDwO9fld7AECSZBPaqoKbr3acw4yfE/D5Q93Rqr75QM6S/OJSfLAxEXd3aYgnvzlo9rwW9fxwJr0ikJdPWZcHN6YyJ/7e7pXe/0zx83SXbXxaTpq58TKqRZKuwyPP8LjJhp6slSr523eGTXaZuSGnI4TAxKX78N5vpzBlpXydEDetBise641ezco+8b49qkOlx//9zzV8saOsun/9kSto9erveOCLvXh46X6Lb7aFJTpcsCKjkJiSgytGK3amZhdi/JI9WBx3Bs+a2J3312f7Y9XjffDevzpV+fzOThq8dWscbLjd+2YWwpy//7lmMbAZ16Mi8Hv29paG210igyudezD5BgbP3YbRi3bipR+PVrq/tq09fBlzN5/G/Uv2VsoMSIMF6SfkvCIdAr09cHeXhmYzIqZEhPhiQOt6AMqK0wHgPz0jDfdn5pfIhl2Ma27KBfpIsjhmFsQD5L8Xc+2U96viYlxbi7xJs2NV7TO16UQqzl3Lw/Orj1T75725vmwLkn8v3mXxPOOMjHQ0zM/MWkbmHm8pi2Mu8yPdQdx47zppzZL09yptiy373Un3wrLl9ewozNyQ01l7+DL2nZNfIF4c1gb3dGmIwhIdWtUPwJcTe2D/+esY2DoMOr1A7O+nMGNEW8zdfLpSQWe5k1ezcexyNjpFBJm8f+aaBPx88DJWPd7HbPHjxev5GDZ/OwK93XHkjaF477eTSErLRYKFmQL3dGmI+oHeDs0a1aaIEB/c3jYMuUWleHd0R4z8ZAcCvNxxV+eGlYajqqLRVNTZtJDMBIpqHGK43Vny+yrPDkk/Dcefr6j9qU2L487gYPINLLq/GxJTzO8jJC0O9nDT4M17OuCng5fwQJ/GNW7D5w/1wLErWejWOES2IF/ZHlYVhcPlpBmgAK+K236y4EZ+QZNmQsxlbqQzt6QzaGprEqN0kckSK4t0j16yfWhz4V//IMjXExuPm97t25hx0CL97zDOnJniLxtGNJ/FkQZ3Ur6y155RcKM1nbnx96rI4gV4uSOjtOz3WdVUcmn9jT23nqguBjfkNLILS7DlZCrWHqp8gezeJASRoRUrrQZ4e+D2tmVFww/3bYaHopvCTavBX4np2H66bH0J6boh5d785TgSU3LQq1koXhreFmsOXYaXuxbP3tEKPx8sW5r94y3/oFvjEGw/nY7bWtdDUlouHl8Rj84RQTh3Lf9mW0ux5WQalvx9DlVx9j1YbKXRaPDVwz0N3x+ZNRRaLVBYooe3hxaFJXq8PbqjYRfqMd0aGf5vGwZ540pWRfq6Z5NQQ6Yj2LfiYttaUvfRrG7FwnFtGwRUGvq6klWAg8k3sPVUGro1CcGgNmF27K157288BQD4LeFqpdeZlHHgMDGmKSbGNLVLG3w83dCzaVnGTPp/Wz/Qy/D/JM1kBBpdLMvJV/s1Wi3Zs+rgRpopysgrlgWttUE6LGlpnSFj/1t/HGN7ROBGXgl6NQuFp7vWMNusVKfH9B+OoFvj4LL3mC/34u9/yoYcLWU0GgX7GPZikma0yv4/Kv5TZMGNFZkbb083eLhpUHJzg9PyvzXAfObH8rCUNHMjXYZAnsXJuBmsBnq7G8oBqiqANvc6qU0MbshpTFl5yBCYGOsSEWzxseXrRwxoXc/wHK+ObIdZ644DKEvbr9p/0bAL75ZTadgiWV7873/SZc/1f3FJmP/nP7i/d2PEnUrDlaxC2bRHAJhkYYfcMVGN0LSuH+b9eRrPGE2DVpvyFLSXuxvWT+kHHw83+Hi6GYKbYR3CDcFN54hgXMmq+NTbrUmIIbiRXjDq+Hnimdtb4mpWIdpLhsHqmVi+XwhgzP+VDQ/4ebrh6P+GOXw9EensnJTswkpbUYQFeBk+/Qb7VGQVjPeQsqcvH+6J6T8cwfNDWt8saC/LTEgvfNJ1bqT1N9Lbbkabbkov0OaGG6QBUUZuMRoF+1T6e6kt5jK3pizbdd6ww/1DfZrg14SruJ5XjNfvao/mdf2w/sgVrD9yBf/uHmEIbABYnEIf5ONhCG68jGZuSQM+aXZDWn8jnSJuHHR6umlRoiuf4eSOwpLiSufJAyDzmRvpn4ivbD0c00FXoE9FraOvpzuKSnWGn2OMwQ3RTUcuZlYKbN7/dye8/FMCnh/S2uox3InRTZBbWIq+Leugc0Qw9p27jobBPmhRzw+r9l80+7iDyZmG25duFBjeyFbuTbapH2+P7ogD56/jrdEd4XezQNSWcWtXV76CrvTiL63hMF5n5Nk7WqK4VI+RncNxObMiI+PlrsXzQ9sYvh/Uph62JqZjQnRTrD5wyXC8eV0/nJWkw/OKdYg/fx0r9yUjxNcTb9zd3q5rHh2+mAkvdy0aSqa55haWVrqQN63jZwhu2jaQrCrswNWW2zUIxO9T+wMoq0nafHPndun0ceneVAFmsjjywRP5hcrcIoBuWo1h2CIixAeP9G2GF1YfwVAFlmSQzlazZf2WFXsuGG6/veGEYSYbAOyvYoaVNGsmzUDKsyVC9vqXvi/I6rIkU8SlwYUGGni6aw3FvtLfi/Tx3h5uKNGVBXjSzI2lndONh6UMt828RrzctdAAhp9jzBne8xjc3IJ0eoHcolIEeLnj233JqOvniRGdGsjOKSzRYfWBS8gvKsXjtzVHblEpPNy0DhlLLS7V4+0N8qmDfZqHYlyPSAxqEyabPlsVdzctpg6uyJQsvDnjJKewBAu3JiE1qwgPRTfBlzcLjqWfsMtJx45N6d+qriH46deyrmGn77dGdcBDfZrgoT5NDOd6ujvveiyOpNVqMK5HBI5czEJMi7qY3L8Zlu06j2dub4WCEh3+/ucaYlrUga+nO2bdXTbjxc+rYs8a44Dks4d6ID23qPI0etkeSmVrtdwn2cX43u4R6NjIdI2VrbLySzB60U4AwJbnK9ZtWbrzXKXZJdJgvJPk59dWge1Tg1riyKVMDO/YAE3qVAzrSYvBpRcrac2GcRulfZHOlHvzng54Y31ZZtRNq8EvU/ph0dYkPD+0NZrV9UPb8ACTO3Q7WvO6/oatD2qy3cGesxUBzaPLzGdpASDU39MQ3Jjb3BSQh43S+/y95Fmc8plP0uCibIuOiloq6e9MGpD4eFQs1me89YY55gqKA2S3K4I2rVYDLw8tzK3bZ23dkyMxuLnF6PQCD36xF7uNNnX74N7OyC4owY6ka5h6Ryss2PKP4Q0ixNcT7288hVA/T/w+tb/sE0BiSg5C/DwQFlC9YtlZ645h+e6yT0xuWg02PXcbMvOL0bp+ADQaTZVrqlgrwNsDG6b0R15xKTzdtfgh/iJCfD3xzWO98fzqw9hfRVHqzDvb4r3fTuGJAc3xUJ8mGPzRNnRqFITPHuqOez/dDQ83DR7s3cTic9xqPri3i+H2zDvb4bkhreHr6Y6PxnXF6gMXMbZ7pOz8tuGB+PTB7rLNIct5umsNgc1rI9vhnV9P4uPxUVj4V8XWDoPahuHXo/I1RxJTchDg7Y4Ab49KO5vbKi2nIrN0WlJAbGrarHSYJizAC72bheJyZgGaWNih2578vdzx7aQ+AMouNB0bBSLQ2wOt6lcEG9I6IekFzTgekF4gpXtbSfvi5+WO9g0DseiBiunr9goqrfXbs/3xy9Er+O/AFrK9kKRa1/fH6dRck/fVRB2/ig9gQbLMjfzDoLmMprk1b6TDVVqNRpYRqRfgZeiLLLiRrW0jzfwY0ZgrKJbcNjO7TgPA08LyC7aufO0IDG5uMZ9uO1MpsAEgm05rvOHbSz+V3ZeRV4w5mxKx4chVtAkPwIvD2uDuT3YgxM8Tf780CAeTbyArvwTDO4ZDo9HIloE/lHwDoX6eaFLHD4cvZuJsei7ahgcaAhugbAuFFlVsxFgTQb4ehjeePTPugLeHG9y0GvzwRDQKSnRIyy7Cm78cx9ab/R/TrRG0Gg2eHNACLcP8cXvb+ogM9YGXuxsOvDbE8Phfn+kHAE69aq7SNBqNYSy/XoAXnhrY0uR5w63YJf2xfs0wOqoR6vp7oZ6/F+7/Yg+eGdRSlu3x8Shb0bd8uq+nuxbrnu4LoCzAr86FVzrccayKRQSDfaV1NlqserwPSvVCkU1APdy0+GVK2WtUo9Hgrs4NsO/cdQzvGI45fySWHZdc+rILSwz/f0Dl4Y//9IzEmfRc9GtZFz88EY1DyTfQV7LmjlLaNwxE+4aV15iSMld4W1N1JIGzdKNYaTBSqheymhvpa0HarshQXySmlgXP0qUTjEqh0Lp+AHYmZVR6vI+Z2W2VsliSxkiDMGnNjZ+s6Fk+JGlcTyRVxMwN1aa//0k3vJmV+2hcFyzY8k+lzR0B03vPfLbtLADgcmYB/rpZkJueU4S2r280nPPc4NZoFOKD/60/ju5NQnB/78b47zcHEOjjgY//E4XJy+MrzS7xdNPiucGt7dJPa0j/aMsvvE3rumNI+3BDcDPtjtZoLPl0Kk2xSx/PoKZ2aTQa1L1ZWBzdog6OvDEUAV7u+GrnecM5zw9tLZsaXVyqx4gFfwMoS8///FRfdDWxfo4l0inO3xvVbzUI8jbsgwUA7RrIF4fTaDSV1o+pTdLAb+H93aDTC9kwxeXMir//U1dzMLZHhOGDR58WZcsitL6Z9Zn9786Gc3s1CzWsOeVMyrN75VnoctIMR3TzOoYPeuWLbFbFeFG+ctL9v6SBjvT9MzO/RFboHiYZbpe2S5oRS5VkC3MKS2UrUUuzaNJsS5/mdXDqZmZROrpraV8taaG7dOmCAFktjzyYsbRwJjM3VCvSc4rw2Nf7Des6DGlfHzEt6uDyjQKM6toILer5491fT6Jfq7ro07wO1h2+jJZh/hjeMRxj/m8XMvNLMP8/XfHOrydw8XrVMyDm/XnacHvb6XRsu1konJlfgglf7ZOd2zDIGysn94Gfl7tNtTWOcl/PSOj0erRvGCgLbMh5la/dMrJTA8zbfBp9mofKApcODQNx/EpFPY9eAHP+OIXCEj2a1/XD/+7pgHs/3Y2TV7Px4rA2eHqQPKv0T2oOEi5nyd6wjS+EIb6esuBmTLcIzP/zH9lML2diPJusuFSPTo2CkHA5Cz2ahmDmne3g4abFsA7hCPT2wIm3himSdaqux/o1w+1tw9C0jh8+3XbG8GFKOvzSIKhi+LN5XX9cy7VcNOzhppEVVI/qWrGuk3QNK+nt06ny9Y9ua1UXWxPTEeTjgZiWFdkuaXAjbVd6dkVRy4WMfNlwZ2SIfGmMch0aBmLh/VH46cAl3NmxAcKDvBH720k80rcZJvdvjglf7cMTtzXH5pMVs0XNZZGk7ZIGMxqN0X5iRlP/GdxQrfhg4ylDYOPj4YaXhrWRLTveJTIYPzwZbfhe+kls6wsDkVdUijr+XujQMBC7zmTg7s4N8fiKeENRbfkLu0eTEDSt64cfb85m8XTXop6/F67lFskyNeGB3ijVC2TkFeGNezqgqWQdE6W5aTV4KLqp0s2gaggP8sbuGbfD02h9o6cHtcRT38qXyC9P5x+4cAO7zmQYpu7O+SMRd7QLw0s/HkWPJqEY3ysSQ+Ztr/JnS2fIAGWLyu179Q7ZcvvOaO7YLvhyxzlMGdQKvl5uWL77Av7TMxLeHm6GrQ0A++w9VJs0Gg2a3xzibhTsY5hRJ60TkWZbIkJ8sO982e3yZSMAoG/LOobXivGwknRjyOgWFYt+5haVYvqQ1pj352k81q8Zjl7KQu7Nta6m3N4KDYJ98MRtzdGkjh9mjGgLrUYjG46vF+CFSf2a4Ysd5/D80Nb482Qqlvx9Do/2bYajlzIRf+EG2tQPQJ/moejfqi4aBvmge5OKRS/Dg7zRv1U9w35bg9qEydZ+OjxrKNy0GsNECEC+FpKfmdlSxsNQ0oLoIB8P2fYPDG5uWrRoEebMmYOUlBR06dIFn3zyCXr16mX2/NWrV+P111/H+fPn0apVK7z//vu48847a7HFrmPR1iT8eLAs2HhxWBs8OaCFTWuAeHu4GWZIRYT4YlyPsk8LXz/SCynZhWgQ5I3sglKk5RQaAqb7ezfGwQs3cF/PSAR4e0CIsjeFb/ZewLXcYtzTpSGa1fVDUanO5d40ybmVf4L1cnfDkgk9oNMLDG1f3zCT6r1/dcLMNQmyx1w22kpj+Pyy4aujl7LMFqYaK9HpsWRCDzyxIh7je5WtPOwKr+1/d4/AvyX7nU0fUntDw7WloSS4kQ7f1JWsmRQhWSBUOiNPOvNJCPkwj3RYqUGQD8b3aoxfj17BgNb10CDIGxNjmiLIxwPz7uuKycvj8eKwNujeJEQWiDxxcy8xaaFxyzB/3NOlIZ65oxWCfDzKFqZsG4ZujUNQUKzD3M2JGNI+HO5uWqx4rLfhcbe1rofTKTmy1b1NKX//f3l4W/zr/3ahe5MQdJO0SZo5kmaEpFmj3MJSWaDYMMhHFtxwthSA77//HtOnT8enn36K3r17Y/78+Rg2bBgSExMRFlZ5pdFdu3Zh/PjxiI2NxV133YWVK1di9OjROHjwIDp27KhAD5zTgQs38OYvxw0Zm4djmlZKt9eEVqtBw5tvAtJCXQDo1jgE3SR/YBqNBhoNMMEoI+IKb/7kuoZI1lj5Y9ptuJCRj97NQ/H6umN2mZIt3Si0fqA3hrSvjz0z70Cob81mZZF9PTmgBXYkXUP/VnXRMqwiY91Isv5PG0kmW/peZrytxkN9muBQciZahvnjX1GNkHw9H83q+iHUzxOxYzrh7VEdDLNJywuLh7SvjyOzhsqe15hWq8Gf0wcg+XoeOjQMkj3ew01r2CTV28MN74w2vT/d0od7QqupvIyCOVGNQ7Bnxh0I8HaHn5c7PhkfhYJiHQa1CcOwDvWx5+x1xLSog4Ft6iEuMR2t6wdgXI8I/BB/CWN7RCIzv2JotmfTEJy4WjH0W+wEwY1GOHrb1ir07t0bPXv2xMKFCwEAer0ekZGReOaZZ/DKK69UOv++++5DXl4eNmzYYDjWp08fdO3aFZ9++mmVPy87OxtBQUHIyspCYKBjxsPLMxWi/LbhOCBQkdo0/Avz50PI74fknPLnK7+jsESPcxl52HDkimyhM1N1BES3qm/2XMBrN1dPLvfisDYID/TG86uPoFndsuGCx1ccAAD8MqUf7l64AwDw9aO98Niy/SjVC4zu2hCTb2uOT7YkYergViZ3gyfncCY99+aqyfkY/FHZMOPJt4aj3ayyiRAHXhuMd387iXWHr+CPaf2xaOsZrDl0GW+P6oDk6/lY8vc5DGpTD0sf6YUd/1xDeJCXLFBSI51ewE2rgV4vcDmzAJGhvtDrBfaczUDHiCCUlOox4+cExLSog46NgjD9hyNoGx6ATSdS0TY8ABun3Wb3Ntly/VY0uCkuLoavry9+/PFHjB492nB84sSJyMzMxLp16yo9pnHjxpg+fTqmTZtmOPbGG29g7dq1OHKk8i6vRUVFKCqqKMrKzs5GZGSk3YObAxduVLlDbG37d7cIPDGguayqnojKNlF95aejhp2Mz8XeCY1Gg/3nr6NJHV/U8/fCsl3nEezrgX9FReDAhRu4nFmAe7o0xK6ka9iamIanBrZESA3XzqHa9/c/ZQW9nSOCcfF6PrIKStCxURCEEMgv1sHPyx06vcDB5BvoGhkMN40GvyZcRY+mIbI6G6ps37nrGPfZbgBl+wH+9N8Yuz6/LcGNouMC165dg06nQ/368iW669evj1OnTpl8TEpKisnzU1JM79IaGxuLN9980z4NdkIaTcXiTJ7uWjQM9kHHhkGYGNME3Zs43xRNImfQrkGgbOZLeSq/fBNKAHikbzPDbWmtREzLurKZLuRa+reqZ7gdGeqL8qUkNRqNoZjWTauRvRbu7tKwNpvoslqF+SPEt2wPKoUHhZSvuXG0GTNmYPr06YbvyzM39tapURD2vzrYUHCmwc1aE5QHIBVRSHlAUul+VBSslR+TnlvxvNaPqxKRac4wo4NITUL8PLFn5h3ILihVdF0nQOHgpm7dunBzc0NqaqrseGpqKsLDTa9UGh4ebtP5Xl5e8PJy/Popnu5ap1inhYisc3/vxth77jp6O+EidESuysvdDfUClN8VXNFFGDw9PdG9e3ds2bLFcEyv12PLli2Ijo42+Zjo6GjZ+QCwefNms+cTEZlyT5eGWPd0Xyx9pKfSTSEiO1N8WGr69OmYOHEievTogV69emH+/PnIy8vDI488AgCYMGECGjVqhNjYWADA1KlTMWDAAMydOxcjR47EqlWrEB8fj88//1zJbhCRi9FoNOhi4xYMROQaFA9u7rvvPqSnp2PWrFlISUlB165dsXHjRkPRcHJyMrSSVT5jYmKwcuVKvPbaa5g5cyZatWqFtWvXco0bIiIiAuAE69zUttpY54aIiIjsy5brt3NvfEJERERkIwY3REREpCoMboiIiEhVGNwQERGRqjC4ISIiIlVhcENERESqwuCGiIiIVIXBDREREakKgxsiIiJSFQY3REREpCoMboiIiEhVFN84s7aVb6WVnZ2tcEuIiIjIWuXXbWu2xLzlgpucnBwAQGRkpMItISIiIlvl5OQgKCjI4jm33K7ger0eV65cQUBAADQajV2fOzs7G5GRkbh48aLqdhxXc98A9s9VqbVf5dg/16TWfpVTqn9CCOTk5KBhw4bQai1X1dxymRutVouIiAiH/ozAwEBVvqABdfcNYP9clVr7VY79c01q7Vc5JfpXVcamHAuKiYiISFUY3BAREZGqMLixIy8vL7zxxhvw8vJSuil2p+a+Aeyfq1Jrv8qxf65Jrf0q5wr9u+UKiomIiEjdmLkhIiIiVWFwQ0RERKrC4IaIiIhUhcENERERqQqDGyIiIlIVBjdERESkKgxunIRer1e6CQ6RmpqKK1euKN0MqgG1rhZx8eJFnD59WulmUDXxPZMsYXCjsKysLABle16p7Y/10KFD6NWrF06dOqV0Uxzi/PnzWLJkCT7++GP8/vvvSjfH7q5fvw4A0Gg0qgtwDh06hB49eiAhIUHppjhEUlIS5syZg5dffhkrVqzAtWvXlG6S3fA903XV6numIMUcP35cBAUFiXfffddwTKfTKdgi+zl8+LDw8/MTU6dOVbopDnH06FERFhYmBg0aJAYOHCi0Wq146KGHxN69e5Vuml0cP35cuLu7y35/er1euQbZUflr87nnnlO6KQ6RkJAg6tSpI0aMGCHGjBkjPD09xe233y7Wr1+vdNNqjO+Zrqu23zMZ3Cjk4sWLIioqSrRu3VqEhoaK2NhYw32u/sd67NgxERAQIF555RUhhBClpaXi0KFDYufOneLYsWMKt67mrl27Jrp06SJeffVVw7HffvtNaLVacffdd4u//vpLwdbV3OXLl0WvXr1Et27dhJ+fn5g2bZrhPlcPcE6ePCl8fX3FzJkzhRBClJSUiG3btom1a9eKnTt3Kty6mrtx44aIiYkx9E+IsmDHzc1NdO/eXSxfvlzB1tUM3zNdlxLvmQxuFKDT6cT8+fPFmDFjxF9//SVmz54tAgMDVfHHWlhYKKKiokSDBg3E1atXhRBCjB49WkRFRYnQ0FDh5+cnPvjgA4VbWTNJSUmie/fu4vjx40Kv14uioiJx5coV0aFDBxEeHi7GjBkjrl+/rnQzq0Wv14tvvvlGjB07VuzcuVOsXLlSeHl5ybIcrhrgFBUViVGjRomwsDCxb98+IYQQd999t+jSpYsICwsTHh4e4tlnnxXp6ekKt7T60tLSRFRUlIiLixM6nU7k5eWJkpIS0b9/f9G1a1cxZMgQcfz4caWbaTO+Z/I901YMbhRy+vRpsXLlSiGEENevXxexsbGq+WPdunWraNOmjfjPf/4junXrJoYOHSr+/vtvsX//fvHxxx8LjUYjFi9erHQzq+3QoUNCo9GILVu2GI4lJSWJ4cOHi2+//VZoNBrx+eefK9jCmrlw4YJYt26d4ftvv/1WeHl5qSKDs3//fjF06FAxfPhw0bZtWzF8+HBx4MABcf78ebF+/Xrh4eEhXnvtNaWbWW1nzpwR3t7e4ocffjAcO3/+vOjdu7f49ttvRXBwsHjrrbcUbGH18T2T75m2YHCjIOkFIj09vdKnkdLSUrF+/XqX+SQp7c/WrVtFeHi4GDBggLhy5YrsvOeff1506tRJZGRkuORFsqSkRDz00EOiZcuWYuHCheK7774TISEh4qmnnhJCCDFt2jTxn//8R5SUlLhk/4SQ/y5LS0srZXBKSkrEN998IxISEpRqYrXt379fxMTEiCFDhohz587J7luwYIGoV6+euHz5ssv+7p577jnh5eUl3njjDfHxxx+LoKAg8cQTTwghhJgzZ47o27evyMvLc8n+8T2T75nWcndsuTKVu3LlCi5fvoyMjAwMHjwYWq0WWq0WpaWlcHd3R926dfHoo48CAN577z0IIZCRkYEFCxYgOTlZ4dZbJu3bHXfcAQAYOHAgNmzYgBMnTqBevXqy8729veHr64uQkBBoNBolmmwTaf+GDBkCd3d3vPzyy1i0aBHeeOMNhIeH46mnnsI777wDoGw2x40bN+Du7hp/XhcvXsTJkyeRnp6OIUOGIDg4GJ6enobXppubG8aOHQsAeOSRRwAAOp0OixcvRlJSkpJNr5K0b4MHD0ZQUBB69OiBzz77DImJiYiIiABQNt1do9FAo9GgQYMGqFOnjku8No1/d6GhoXjrrbcQGBiI5cuXo379+pg+fTpmzZoFoGIGnK+vr5LNtgrfMyvwPbMa7BIikUVHjhwRkZGRon379sLd3V1ERUWJxYsXi5ycHCFE2aeNcunp6SI2NlZoNBoREhIi9u/fr1SzrWKqb4sWLRJZWVlCCCGKi4srPebJJ58Ujz76qCgqKnL6TyHG/evatav4/PPPRX5+vhBCiEuXLsk+Zen1ejFhwgTx8ssvC71e7xL9q1+/vujWrZvw9PQUHTp0EC+++KK4ceOGEEL+2iwtLRUrVqxwqdemcd+ef/55kZGRIYQw/dqcOnWquPfee0VeXl5tN9dmxv1r166dePnllw2/u/T0dMPtco8//riYNGmSKC4udurXJt8z5fieaTsGNw6Wnp5ueNM5d+6cSEtLE+PHjxe9e/cW06ZNE9nZ2UII+VjxQw89JAIDA52+8M/avpW7cuWKeP3110VISIjT900I8/3r2bOnmDZtmsjMzJSdf+bMGTFz5kwRHBwsTpw4oVCrrZeZmSm6detmuOAXFBSIGTNmiJiYGDFq1ChDEFB+IdHpdOKxxx4TgYGBTt8/a/tW7uzZs+L1118XwcHBLjE7xVz/oqOjxT333COuXbsmhKgY9vjnn3/ESy+9JAIDA52+f3zPrMD3zOpjcONgCQkJomnTpuLIkSOGY0VFRWLWrFmiV69e4tVXXxUFBQVCiLI3ohUrVoj69euLAwcOKNVkq9nSt3379omxY8eKiIgIcejQIYVabBtb+peeni6efPJJ0aZNG3Hw4EGlmmyTc+fOiebNm4u4uDjDsaKiIvHVV1+J6Oho8cADDxjebPV6vfjtt99Es2bNnP6TsRC29S0hIUHcc889omnTpi7z2rTUvz59+oj777/f0L+MjAzx2muviR49erjEa5PvmXzPtAcGNw6WmJgomjVrJn755RchRFlhVfm/L774oujatavYvn274fyzZ8+K8+fPK9JWW9nSt4sXL4rVq1eLpKQkxdprK1t/d2fOnBGXLl1SpK3VkZ6eLjp27Cg++eQTIUTFp3ydTicWLVokunXrJlsXJSUlxTBV1dnZ0rf8/HyxZcsWcfbsWcXaaytbf3eXL18WqampirTVVnzP5HumPTC4cbDCwkLRo0cPcddddxnS++W/cL1eLzp16iQmTJhg+N6VWNO3hx56SMkm1ogtvztXVFxcLP7973+LmJgYkxeHoUOHipEjRyrQspqzpm933nmnAi2zDzX/7vieyfdMe+DeUg6k1+vh5eWFpUuXYvv27fjvf/8LAHB3dzfMzrjnnnuQlpYGAC5RBV/O2r6lp6cr3NLqsfV352qEEPDw8MD//d//4cyZM3j22WeRlpYm20Pq7rvvxrVr11BYWKhgS21nbd8yMjJcrm+Aun93fM/ke6a9MLhxIK1WC51Oh44dO+Lrr7/Gd999hwkTJiA1NdVwzrlz5xASEgKdTqdgS22n5r4B6u+fRqNBcXExwsLCsHHjRuzduxcPPvgg4uPjDf05fPgw6tSpA63Wtd4m1Nw3QN39U/PfnZr7Bjhf/zRCqGy7XydSvh5Dbm4uioqKcPjwYdx///1o0qQJQkNDUadOHaxbtw67d+9Gp06dlG6uTdTcN0D9/dPpdHBzc0NGRgaKi4tRUFCAESNGwN/fH6WlpWjevDm2bNmCHTt2oHPnzko31yZq7hug7v6p+e9OzX0DnK9/rhXWOynj+FAIYfhFnz9/Hq1bt8b+/ftxxx134Pjx47jzzjvRqFEjhIWFYd++fU79QlZz3wD198+U8ovj+fPn0blzZ2zZsgXNmzfH/v37MW3aNAwZMgQ9e/bE/v37Xe7iqOa+Aerun5r/7tTcN8A5+8fMTQ0lJibi22+/RXJyMvr164d+/fqhbdu2AIDk5GR069YNo0ePxpIlS6DX6+Hm5mYYf9Tr9U6dNlZz3wD19y81NRVZWVlo3bp1pfsuXbqETp06YezYsfjss88ghHD6/kipuW+Auvt37tw5/PHHHzh9+jRGjBiBqKgo1K1bF0DZisvdunXDqFGjXPLvTs19A1ysf7VQtKxax48fF0FBQYZZC7179xYRERFi8+bNQoiyfWqmTZtWqaK//HtnrvRXc9+EUH//Tpw4IRo3bizGjRtnctG2NWvWiOeff97p+2GKmvsmhLr7d/ToUdGwYUMxYsQI0apVK9GmTRvx/vvvi9LSUlFcXCwWLlwonnvuOZf8u1Nz34Rwvf4xuKmm0tJS8eCDD4oHHnjAcOzQoUNi0qRJws3NTWzatMlwnqtRc9+EUH//Ll++LGJiYkSXLl1Er169xGOPPVZpg0tTS7y7AjX3TQh19+/8+fOiVatWYubMmYY+vPLKK6Jly5aGhd2MV7B1FWrumxCu2T/nzoE5Mb1ej4sXLyIyMtJwrGvXrnjvvfcwefJkjBo1Cnv27IGbm5uCraweNfcNUH//Tp06hYCAAHz99dd46qmncOjQIcyfPx/Hjh0znOPh4aFgC6tPzX0D1Ns/nU6HdevWISoqCs8884xheGLatGkoLi7G6dOnAQBBQUFKNrNa1Nw3wHX7x+Cmmjw8PNCxY0ds27YNN27cMByvV68eZs6ciTvvvBNvv/02srOzFWxl9ai5b4D6+xcTE4M33ngDXbp0wcSJEzFlyhTDRTIhIcFwnrhZbqfX65Vqqs3U3DdAvf1zc3NDUFAQ+vbti/DwcMMHB41Gg+zsbMNu5VLCRcpB1dw3wIX7p2TayNV9//33IioqSsydO7fShmfLli0TDRs2FMnJyQq1rmbU3Dch1N8/4/HtZcuWiW7dusmGOd58803ZHjCuQs19E0L9/ROioo8FBQWibdu2Yu/evYb71q1bp4q/PTX2TQjX6Z+70sGVq7hy5QoOHjyI4uJiNG7cGD169MC4ceMQFxeHJUuWwMfHB/fddx9CQ0MBAD179oSvry9ycnIUbnnV1Nw34NbqX5MmTdC9e3doNBqIspo6aLVaTJw4EQDw8ccfY8GCBcjOzsaPP/6Ie++9V+HWW6bmvgHq7p+pvzugYjo7ULbwm1arNaw0PHPmTCxduhR79+5VrN3WUHPfAJX0T8nIylUcPXpUNG/eXPTq1UvUrVtX9OjRQ3z33XeG+x9++GHRqVMnMW3aNJGUlCTS09PFSy+9JFq3bi2uXbumYMurpua+CXFr9m/16tWyc3Q6neH2l19+KTw8PERQUJDT7zSs5r4Joe7+WdM3IYS4ceOGqFevnti5c6d4++23hbe3t9PvOq/mvgmhnv4xuKlCUlKSiIiIEC+99JLIzMwU8fHxYuLEieLRRx8VhYWFhvPefPNN0b9/f6HRaET37t1FeHi4Q7Zxtyc1902IW7t/paWlsuENvV4vSktLxbPPPitCQkJMTjF2JmrumxDq7p8tfcvJyRFRUVFi4MCBwtvbW8THxyvY8qqpuW9CqKt/DG4sKCoqEtOnTxfjxo0TRUVFhuNffvmlqFOnTqVP9teuXRO///672LFjh7h48WJtN9cmau6bEOyfqazTvn37hEajcapPV6aouW9CqLt/tvYtMzNTNGnSRISGhorDhw/XdnNtoua+CaG+/rHmxgK9Xo+IiAi0a9cOnp6ehpUWY2Ji4O/vj5KSEsN5Wq0WderUwfDhwxVutXXU3DeA/Svvn1TPnj1x/fp1BAcH136DbaDmvgHq7p+tfQsKCsLkyZPx73//27A6uLNSc98AFfZPsbDKRZw9e9Zwuzwld/XqVdGyZUtZVbgrDGMYU3PfhGD/ykn75+yroJZTc9+EUHf/rO2bs2ehTFFz34RQV/+4zo2Rq1evYt++fdi4cSP0ej2aNWsGoKxKvLwqPCsrS7Y+yqxZs3DHHXcgIyPDOeb3m6HmvgHsH1B1/8rPczZq7hug7v5Vt29Dhw51+r87NfcNUHn/FAurnNCRI0dEkyZNROvWrUVQUJBo27atWLlypcjIyBBCVESyiYmJol69euL69evi7bffFj4+Pk5XTGVMzX0Tgv1z5f6puW9CqLt/7Jtr9k0I9fePwc1NaWlpom3btmLmzJnizJkz4vLly+K+++4T7dq1E2+88YZIS0sznJuamiqioqLEfffdJzw9PZ3+F63mvgnB/rly/9TcNyHU3T/2rYyr9U0I9fdPCAY3BsePHxdNmzat9It7+eWXRadOncQHH3wg8vLyhBBlu/ZqNBrh4+Pj9OtNCKHuvgnB/rly/9TcNyHU3T/2zTX7JoT6+ycEa24MSkpKUFpaivz8fABAQUEBAGD27NkYNGgQFi9ejKSkJABASEgInnrqKRw8eBBdu3ZVqslWU3PfAPbPlfun5r4B6u4f++aafQPU3z8A0AjhzBVBtatXr17w9/fHX3/9BQAoKiqCl5cXgLKpmC1btsR3330HACgsLIS3t7dibbWVmvsGsH+u3D819w1Qd//YN9fsG6D+/t2ymZu8vDzk5OTIdn7+7LPPcPz4cdx///0AAC8vL5SWlgIAbrvtNuTl5RnOdeZftJr7BrB/gOv2T819A9TdP/bNNfsGqL9/ptySwc2JEycwZswYDBgwAO3atcO3334LAGjXrh0WLFiAzZs3Y+zYsSgpKYFWW/ZflJaWBj8/P5SWljr19Dc19w1g/1y5f2ruG6Du/rFvrtk3QP39M0uhWh/FHD9+XNSpU0c899xz4ttvvxXTp08XHh4ehsWy8vLyxPr160VERIRo27atGD16tBg3bpzw8/MTCQkJCrfeMjX3TQj2z5X7p+a+CaHu/rFvrtk3IdTfP0tuqZqb69evY/z48Wjbti0WLFhgOD5o0CB06tQJH3/8seFYTk4O3nnnHVy/fh3e3t7473//i/bt2yvRbKuouW8A++fK/VNz3wB19499K+NqfQPU37+q3FJ7S5WUlCAzMxP33nsvgIp9hZo1a4br168DAETZ9HgEBATg/fffl53nzNTcN4D9A1y3f2ruG6Du/rFvrtk3QP39q4rr98AG9evXxzfffIP+/fsDKFtiGgAaNWpk+GVqNBpotVpZ4ZWzLnsupea+Aewf4Lr9U3PfAHX3j31zzb4B6u9fVW6p4AYAWrVqBaAsOvXw8ABQFr2mpaUZzomNjcUXX3xhqBx3lV+2mvsGsH+A6/ZPzX0D1N0/9s01+waov3+W3FLDUlJarVa2GV15JDtr1iy88847OHToENzdXfO/R819A9g/V+6fmvsGqLt/7Jtr9g1Qf/9MueUyN1LltdTu7u6IjIzEhx9+iA8++ADx8fHo0qWLwq2rGTX3DWD/XJma+waou3/sm+tSe/+MqStUs1F59Orh4YElS5YgMDAQO3bsQLdu3RRuWc2puW8A++fK1Nw3QN39Y99cl9r7V4kDppe7nP379wuNRiOOHz+udFPsTs19E4L9c2Vq7psQ6u4f++a61N6/crfUOjeW5OXlwc/PT+lmOISa+wawf65MzX0D1N0/9s11qb1/ADfOJCIiIpW5pQuKiYiISH0Y3BAREZGqMLghIiIiVWFwQ0RERKrC4IaIiIhUhcENERERqQqDGyJyGQMHDsS0adOUbgYROTkGN0SkSnFxcdBoNMjMzFS6KURUyxjcEBERkaowuCEip5SXl4cJEybA398fDRo0wNy5c2X3r1ixAj169EBAQADCw8Nx//33Iy0tDQBw/vx5DBo0CAAQEhICjUaDhx9+GACg1+sRGxuLZs2awcfHB126dMGPP/5Yq30jIsdicENETunFF1/Etm3bsG7dOmzatAlxcXE4ePCg4f6SkhK8/fbbOHLkCNauXYvz588bApjIyEj89NNPAIDExERcvXoVCxYsAADExsZi+fLl+PTTT3H8+HE899xzePDBB7Ft27Za7yMROQb3liIip5Obm4s6dergm2++wdixYwEA169fR0REBB5//HHMnz+/0mPi4+PRs2dP5OTkwN/fH3FxcRg0aBBu3LiB4OBgAEBRURFCQ0Px559/Ijo62vDYSZMmIT8/HytXrqyN7hGRg7kr3QAiImNnzpxBcXExevfubTgWGhqKNm3aGL4/cOAA/ve//+HIkSO4ceMG9Ho9ACA5ORnt27c3+bxJSUnIz8/HkCFDZMeLi4sRFRXlgJ4QkRIY3BCRy8nLy8OwYcMwbNgwfPvtt6hXrx6Sk5MxbNgwFBcXm31cbm4uAODXX39Fo0aNZPd5eXk5tM1EVHsY3BCR02nRogU8PDywd+9eNG7cGABw48YNnD59GgMGDMCpU6eQkZGB2bNnIzIyEkDZsJSUp6cnAECn0xmOtW/fHl5eXkhOTsaAAQNqqTdEVNsY3BCR0/H398djjz2GF198EXXq1EFYWBheffVVaLVlcyAaN24MT09PfPLJJ3jyySdx7NgxvP3227LnaNKkCTQaDTZs2IA777wTPj4+CAgIwAsvvIDnnnsOer0e/fr1Q1ZWFnbu3InAwEBMnDhRie4SkZ1xthQROaU5c+agf//+uPvuuzF48GD069cP3bt3BwDUq1cPy5Ytw+rVq9G+fXvMnj0bH374oezxjRo1wptvvolXXnkF9evXx5QpUwAAb7/9Nl5//XXExsaiXbt2GD58OH799Vc0a9as1vtIRI7B2VJERESkKszcEBERkaowuCEiIiJVYXBDREREqsLghoiIiFSFwQ0RERGpCoMbIiIiUhUGN0RERKQqDG6IiIhIVRjcEBERkaowuCEiIiJVYXBDREREqvL/fw7LsBqgXnMAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -419,7 +433,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 25, "metadata": { "id": "LqqHzjty8jk0" }, @@ -439,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 26, "metadata": { "id": "g4MeM8Oe9Q6X" }, @@ -479,7 +493,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 27, "metadata": { "id": "x95ZgBkyDMP4" }, @@ -500,7 +514,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 28, "metadata": { "id": "V0OK02D7PJSL" }, @@ -508,7 +522,7 @@ { "data": { "text/html": [ - "Query job 44159a16-cab9-4ffa-be68-2228387a48c2 is DONE. 12.6 GB processed. Open Job" + "Query job 5b76ac5f-2de7-49a6-88e8-0ba5ea3df68f is DONE. 129.5 MB processed. Open Job" ], "text/plain": [ "" @@ -545,7 +559,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 29, "metadata": { "id": "EG7qM3R18bOb" }, @@ -556,13 +570,13 @@ "" ] }, - "execution_count": 14, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -586,7 +600,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 30, "metadata": { "id": "5nVy61rEGaM4" }, @@ -597,13 +611,13 @@ "" ] }, - "execution_count": 15, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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IhFs2rQp9jM7d+7MqJo3RIn67LPPMG3aNEybNg2AXgZh2rRpuPPOOwEAa9aswY9+9CPceOONGD9+PBYtWoRPP/0Uw4cPBwDIsoxXX30VRUVFmDt3Ls4++2xUVFTg2Wef7fFYODNDRDHz58/H7NmzsWjRIvz2t7/FuHHjcOjQIbz22mtYvHgxZsyYAUDfEfXQQw/Fdh0UFhaioqICzz33XMIFtC688ELce++9WLRoEVasWIGysjJs3rwZ5eXlmD17Nv7nf/4H3//+9zFt2jTMnz8fr776Kv75z3/i3Xff7bPn31u//OUvMWvWLFx77bW44oorkJOTg+3bt+Odd97BH//4R4wfPx4LFy7E1VdfjdWrV8NgMOCGG25I2SwV0bF06qmnQk+X6ZjRaMTy5cuxfPnyTs8pLy/HP/7xj16PhTMzRBQjSRJef/11zJ07F5dddhnGjRuHCy64APv27YvlhwB63oyqqnG5Maeeemq7Y10xmUx4++23UVxcjLPOOguTJ0/GypUroSgKAGDRokV48MEHcf/992PSpEl49NFHsWbNmoTvPx2mTJmC9evXY9euXTjllFNif6WWl5fHzlmzZg3Ky8sxb948nHfeebjqqqtQXFycxlETZT9JdBVWEREREWU4zswQERFRVmMwQ0R94t5770Vubm6HX131cyEi6ikuMxFRn2hqaup0p5LVasWQIUOO8YiIqL9iMENERERZjctMRERElNUYzBAREVFWYzBDREREWY3BDBEREWU1BjNERESU1RjMEBERUVZjMENERERZjcEMERERZbX/H7fM0QLZs4TYAAAAAElFTkSuQmCC", + "image/png": "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691Mdrsd//nPfzB69Gjk5OR0+RHxwAMPoLW1NZnjJeqTg774EnvjPY+IiFIvoa3Zc+fOjatL9v33389EYUovQkvueURElHIJBTPxEoI7Qii9eINxVgCO8zwiIkq9fg1miNKNLxRfgB3veURElHoMZmhQifcFz18MIqLMwfdsGlSUOF/x8Z5HRESpx7dsGlTMRimp5xERUer1azBz8sknw2plJVVKH23++BJ74z2PiIhSL6Fg5osvvsDmzZujn7/66qtYsGABbr/9dgSDwejxN998E6WlpX0fJVGSCCm+GZd4zyMiotRLKJi55pprsGPHDgDArl27cOGFF8Jms+Gll17CLbfcktQBEiWTLMW3Syne84iIKPUSCmZ27NiB4447DgDw0ksvYc6cOXjuuefwzDPP4B//+Ecyx0eUVKoaX5AS73lERJR6CQUzQghoml4h9d///jfOOussAMDw4cPR1NSUvNERJZkrGF+QEu95RESUegkFM9OnT8evfvUr/PWvf8WaNWtw9tlnAwB2796N4uLipA6QiIiIqCcJBTPLli3DF198geuuuw533HEHxo4dCwD4+9//jtmzZyd1gEREREQ9OXK3yC5Mnjw5ZjdTxIMPPghFUfo8KCIiIqJ4JRTMdMdisSTz4YiIiIiOKO5gJi8vD1KctTdaWloSHhARERFRb8QdzCxbtiz6383NzfjVr36FM844A7NmzQIArFu3Du+88w7uuuuupA+SKFkkAPHsU2LJPCKizCEJIXq9B/X888/Ht7/9bVx33XUxx//whz/g3//+N1555ZVkja/PXC4XcnJy4HQ64XA4Uj0cSrGxt76BcBznGQBUP3B2fw+HiIi60Zv7d0K7md555x3Mnz//sOPz58/Hv//970QekuioiCeQ6c15RESUegkFMwUFBXj11VcPO/7qq6+ioKCgz4MiIiIiildCu5mWLFmCK6+8EqtXr8aMGTMAAJ988gnefvttPPnkk0kdIBEREVFPEgpmLrvsMlRWVuL3v/89/vnPfwIAKisr8eGHH0aDGyIiIqKjIeE6MzNmzMCzzz6bzLEQERER9VrCwYymaaiurkZDQ0O06WTEnDlz+jwwIiIiongkFMx8/PHHuPjii7Fnzx503tktSRJUVU3K4IiIiIiOJKFg5ic/+QmmT5+ON954A6WlpXFXBiYiIiJKtoSCmZ07d+Lv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z7/++usxYcIEWK1WjBgxAjfccAOcTmfMeZIkHfaxcuXK/n46CXnssccwatQoWCwWzJgxA59++mmP57/00kuoqKiAxWLBpEmT8Oabb8Z8XQiBu+++G6WlpbBarZg3bx527tzZn08hqXpzPZ588kmcfPLJyMvLQ15eHubNm3fY+Zdddtlhr4X58+f399NImt5cj2eeeeaw52qxWGLOGUyvj1NOOaXL94Kzzz47ek4mvz7ef/99nHPOOSgrK4MkSXjllVeO+D2rV6/G8ccfD7PZjLFjx+KZZ5457Jzevielk95ek3/+85/4zne+gyFDhsDhcGDWrFl45513Ys659957D3uNVFRU9G2gggaU+fPniylTpoiPP/5YfPDBB2Ls2LHioosu6vb8zZs3i/POO0+89tprorq6WqxatUqMGzdOnH/++THnARArVqwQtbW10Q+fz9ffT6fXVq5cKUwmk/jTn/4ktmzZIq666iqRm5sr6uvruzz/o48+EoqiiN/85jdi69at4s477xRGo1Fs3rw5es4DDzwgcnJyxCuvvCI2bdokvvvd74rRo0en5fPvrLfX4+KLLxaPPfaY2LBhg6iqqhKXXXaZyMnJEfv374+ec+mll4r58+fHvBZaWlqO1lPqk95ejxUrVgiHwxHzXOvq6mLOGUyvj+bm5phr8dVXXwlFUcSKFSui52Ty6+PNN98Ud9xxh/jnP/8pAIiXX365x/N37dolbDabWLx4sdi6dat49NFHhaIo4u23346e09trnG56e01+9rOfiV//+tfi008/FTt27BC33XabMBqN4osvvoiec88994iJEyfGvEYaGxv7NE4GMwPI1q1bBQDx2WefRY+99dZbQpIk8c0338T9OC+++KIwmUwiFApFj8XzIk4HJ5xwgli0aFH0c1VVRVlZmVi6dGmX53//+98XZ599dsyxGTNmiGuuuUYIIYSmaaKkpEQ8+OCD0a+3trYKs9ksnn/++X54BsnV2+vRWTgcFtnZ2eLPf/5z9Nill14qzj333GQP9ajo7fVYsWKFyMnJ6fbxBvvr4+GHHxbZ2dnC4/FEj2Xy66OjeN7zbrnlFjFx4sSYYxdccIE444wzop/39Rqnk0TvA8ccc4xYsmRJ9PN77rlHTJkyJXkDE0JwmWkAWbduHXJzczF9+vTosXnz5kGWZXzyySdxP47T6YTD4YDB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MJlMYujQoeKCCy4Q1dXVfRonu2YTERFRRmPODBEREWU0BjNERESU0RjMEBERUUZjMENEREQZjcEMERERZTQGM0RERJTRGMwQERFRRmMwQ0RERL32/vvv45xzzkFZWRkkScIrr7zS68cQQuChhx7C+PHjYTabMXToUNx33329fhwGM0Q0oH300UeYNGkSjEYjFixYgNWrV0OSJLS2tqZ6aFGjRo3CsmXLUj0Mol5pa2vDlClT8NhjjyX8GD/72c/w1FNP4aGHHsK2bdvw2muvHdaJPR6GhEdARJQBFi9ejOOOOw5vvfUW7HY7bDYbamtrkZOTk+qhEWW0M888E2eeeWa3Xw8EArjjjjvw/PPPo7W1Fcceeyx+/etf45RTTgEAVFVVYfny5fjqq68wYcIEAHrz1kRwZoaIBrSamhqceuqpGDZsGHJzc2EymVBSUgJJkro8X1VVaJp2lEdJNPBcd911WLduHVauXIkvv/wS//3f/4358+dj586dAIDXX38dY8aMwb/+9S+MHj0ao0aNwpVXXomWlpZe/ywGM0SDzCmnnIIbbrgBt9xyC/Lz81FSUoJ77703+vXW1lZceeWVGDJkCBwOB0499VRs2rQJAOB0OqEoCj7//HMAgKZpyM/Px8yZM6Pf/7e//Q3Dhw+Payz79+/HRRddhPz8fGRlZWH69On45JNPol9fvnw5ysvLYTKZMGHCBPz1r3+N+X5JkvDUU09h4cKFsNlsGDduHF577TUAwNdffw1JktDc3Iwf//jHkCQJzzzzzGHLTM888wxyc3Px2muv4ZhjjoHZbMbevXsxatQo/OpXv8KPfvQj2O12jBw5Eq+99hoaGxtx7rnnwm63Y/LkydFrEfHhhx/i5JNPhtVqxfDhw3HDDTegra0t+vWGhgacc845sFqtGD16NJ599tm4rhVRJtm7dy9WrFiBl156CSeffDLKy8vx85//HCeddBJWrFgBANi1axf27NmDl156CX/5y1/wzDPPYP369fje977X+x/YpzaVRJRx5s6dKxwOh7j33nvFjh07xJ///GchSVK0C/K8efPEOeecIz777DOxY8cOcfPNN4uCggLR3NwshBDi+OOPFw8++KAQQoiNGzeK/Px8YTKZot3Zr7zySnHJJZcccRxut1uMGTNGnHzyyeKDDz4QO3fuFC+88IJYu3atEEKIf/7zn8JoNIrHHntMbN++Xfz2t78ViqKI//u//4s+BgAxbNgw8dxzz4mdO3eKG264QdjtdtHc3CzC4bCora0VDodDLFu2TNTW1gqv1xvtAnzw4EEhhBArVqwQRqNRzJ49W3z00Udi27Ztoq2tTYwcOVLk5+eLP/7xj2LHjh3i2muvFQ6HQ8yfP1+8+OKLYvv27WLBggWisrJSaJomhBCiurpaZGVliYcffljs2LFDfPTRR2Lq1Knisssui475zDPPFFOmTBHr1q0Tn3/+uZg9e7awWq3i4Ycf7ts/LFEKARAvv/xy9PN//etfAoDIysqK+TAYDOL73/++EEKIq666SgCI6TK/fv16AUBs27atdz8/Kc+CiDLG3LlzxUknnRRz7Fvf+pb4xS9+IT744APhcDiE3++P+Xp5ebl44oknhBBCLF68WJx99tlCCCGWLVsmLrjgAjFlyhTx1ltvCSGEGDt2rPh//+//HXEcTzzxhMjOzo4GSZ3Nnj1bXHXVVTHH/vu//1ucddZZ0c8BiDvvvDP6ucfjEQCiYxFCiJycHLFixYro510FMwDExo0bY37WyJEjxQ9+8IPo57W1tQKAuOuuu6LH1q1bJwCI2tpaIYQQV1xxhbj66qtjHueDDz4QsiwLn88ntm/fLgCITz/9NPr1qqoqAYDBDGW0zsHMypUrhaIoYtu2bWLnzp0xH5Hfl7vvvlsYDIaYx/F6vQJA9I+reDEBmGgQmjx5csznpaWlaGhowKZNm+DxeFBQUBDzdZ/Ph5qaGgDA3Llz8fTTT0NVVaxZswann346SkpKsHr1akyePBnV1dXRBL+ebNy4EVOnTkV+fn6XX6+qqsLVV18dc+zEE0/EI4880u1zycrKgsPhQENDwxF/fkcmk+mwa9L5sYuLiwEAkyZNOuxYQ0MDSkpKsGnTJnz55ZcxS0dCCGiaht27d2PHjh0wGAyYNm1a9OsVFRXIzc3t1XiJ0t3UqVOhqioaGhpw8sknd3nOiSeeiHA4jJqaGpSXlwMAduzYAQAYOXJkr34egxmiQchoNMZ8LkkSNE2Dx+NBaWkpVq9efdj3RG64c+bMgdvtxhdffIH3338f999/P0pKSvDAAw9gypQpKCsrw7hx4444BqvVmoyn0u1z6Q2r1dplQnDHx458vatjkZ/n8XhwzTXX4IYbbjjssUaMGBF9oyYaCDweD6qrq6Of7969Gxs3bkR+fj7Gjx+PSy65BD/60Y/w29/+FlOnTkVjYyNWrVqFyZMn4+yzz8a8efNw/PHH48c//jGWLVsGTdOwaNEifOc738H48eN7NRYmABNR1PHHH4+6ujoYDAaMHTs25qOwsBCAHtRMnjwZf/jDH2A0GlFRUYE5c+Zgw4YN+Ne//oW5c+fG9bMmT56MjRs3drtzobKyEh999FHMsY8++gjHHHNM355kPzr++OOxdevWw67d2LFjYTKZUFFRgXA4jPXr10e/Z/v27WlV84YoXp9//jmmTp2KqVOnAtDLIEydOhV33303AGDFihX40Y9+hJtvvhkTJkzAggUL8Nlnn2HEiBEAAFmW8frrr6OwsBBz5szB2WefjcrKSqxcubLXY+HMDBFFzZs3D7NmzcKCBQvwm9/8BuPHj8eBAwfwxhtvYOHChZg+fToAfUfUo48+Gt11kJ+fj8rKSrzwwgtxF9C66KKLcP/992PBggVYunQpSktLsWHDBpSVlWHWrFn4n//5H3z/+9/H1KlTMW/ePLz++uv45z//iX//+9/99vz76he/+AVmzpyJ6667DldeeSWysrKwdetWvPfee/jDH/6ACRMmYP78+bjmmmuwfPlyGAwG3HjjjUmbpSI6mk455RTo6TJdMxqNWLJkCZYsWdLtOWVlZfjHP/7R57FwZoaIoiRJwptvvok5c+bg8ssvx/jx43HhhRdiz5490fwQQM+bUVU1JjfmlFNOOexYT0wmE959910UFRXhrLPOwqRJk/DAAw9AURQAwIIFC/DII4/goYcewsSJE/HEE09gxYoVcT9+KkyePBlr1qzBjh07cPLJJ0f/Si0rK4ues2LFCpSVlWHu3Lk477zzcPXVV6OoqCiFoybKfJLoKawiIiIiSnOcmSEiIqKMxmCGiPrF/fffD7vd3uVHT/1ciIh6i8tMRNQvWlpaut2pZLVaMXTo0KM8IiIaqBjMEBERUUbjMhMRERFlNAYzRERElNEYzBAREVFGYzBDREREGY3BDBEREWU0BjNERESU0RjMEBERUUZjMENEREQZ7f8DTH3rzgZxiDYAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -622,7 +636,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 31, "metadata": { "id": "-S1A9E3WGaYH" }, @@ -633,13 +647,13 @@ "" ] }, - "execution_count": 16, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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QsRsERpJavzGsWrVKsyZMweAf9K8RYsW4c0334TL5cK0adPwhz/8oclmprPZ7XZYrVbYbDY2OREREWlEa+7fMU1mOgKTGSIiIu1pzf27U3QAJiIiIooUkxkiIiLSNCYzREREpGlMZoiIiEjTmMwQERGRpjGZISIiIk1jMkNERESaxmSGiIiINI3JDBEREWkakxkiIiLSNCYzREREpGkRJzOVlZV49dVXsXjxYlRUVAAAduzYgePHj0ctOCIiIqKW6CJ50bfffospU6bAarXihx9+wLx585CcnIx33nkHR44cwWuvvRbtOImIiIgaFVHNzMKFCzFnzhwcOnQIJpMpuP2yyy7DZ599FrXgiIiIiFoSUTLzzTff4I477miwvXfv3igtLW1zUEREREThiiiZMRqNsNvtDbYfPHgQPXv2bHNQREREROGKKJm58sor8eijj8Lj8QAAJEnCkSNHcN999+Gaa66JaoBEREREzYkomfntb38Lh8OBtLQ01NTUYNKkSRg0aBASExPxv//7v9GOkYiIiKhJEY1mslqt+OSTT7B582bs3r0bDocD5513HqZMmRLt+IiIiIiaFVEyEzB+/HiMHz8egH/eGSIiIqKOFlEz01NPPYW///3vwefXXnstUlJS0Lt3b+zevTtqwRERERG1JKJk5o9//COysrIAAJ988gk++eQTfPjhh5gxYwZ+/etfRzVAIiIiouZE1MxUWloaTGbef/99XHvttZg6dSr69++PMWPGRDVAIiIiouZEVDPTo0cPHD16FACwfv36YMdfIQR8Pl/0oiMiIiJqQUQ1M1dffTVuuOEGDB48GOXl5ZgxYwYAYOfOnRg0aFBUAyQiIiJqTkTJzPPPP4/+/fvj6NGjePrpp5GQkAAAKCkpwS9/+cuoBkhERETUHEkIIWIdRHuy2+2wWq2w2WywWCyxDoeIiIjC0Jr7d9g1M+vWrcOMGTOg1+uxbt26ZsteeeWV4R6WiIiIqE3CrpmRZRmlpaVIS0uDLDfdb1iSpE7VCZg1M0RERNrTLjUzqqo2+nciIiKiWIpoaDYRERFRZxHRaKZHH3202f0PP/xwRMEQERERtVZEyczatWtDnns8HhQVFUGn0yE7O5vJDBEREXWYiJKZnTt3Nthmt9sxZ84czJo1q81BEREREYUran1mLBYLli1bhoceeihahyQiIiJqUVQ7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EWlH4L4dzgwyXT6ZqaqqAgAuOElERKRBVVVVsFqtzZbp8pPmqaqK4uJiJCYmQpKkWIcTc3a7HVlZWTh69CgnEQTPx9l4PhriOQnF8xGK5yNUNM+HEAJVVVXIzMyELDffK6bL18zIsow+ffrEOoxOx2Kx8H+8eng+QvF8NMRzEornIxTPR6honY+WamQC2AGYiIiINI3JDBEREWkak5luxmg0YunSpTAajbEOpVPg+QjF89EQz0kono9QPB+hYnU+unwHYCIiIuraWDNDREREmsZkhoiIiDSNyQwRERFpGpOZLqaiogI33ngjLBYLkpKScNttt8HhcDRb/q677sLQoUMRFxeHvn374u6774bNZgspJ0lSg8eaNWva++NE5KWXXkL//v1hMpkwZswYfP31182Wf/vttzFs2DCYTCbk5ubigw8+CNkvhMDDDz+MjIwMxMXFYcqUKTh06FB7foSoas35eOWVVzBhwgT06NEDPXr0wJQpUxqUnzNnToNrYfr06e39MaKmNedj9erVDT6ryWQKKdOdro/Jkyc3+l1w+eWXB8to+fr47LPPcMUVVyAzMxOSJOHdd99t8TUbN27EeeedB6PRiEGDBmH16tUNyrT2O6kzae05eeedd/DjH/8YPXv2hMViwdixY/HRRx+FlHnkkUcaXCPDhg1rW6CCupTp06eLkSNHii+//FJ8/vnnYtCgQeL6669vsvyePXvE1VdfLdatWycKCgrEhg0bxODBg8U111wTUg6AWLVqlSgpKQk+ampq2vvjtNqaNWuEwWAQf/7zn8W+ffvEvHnzRFJSkjhx4kSj5Tdv3iwURRFPP/202L9/v3jwwQeFXq8Xe/bsCZZ58sknhdVqFe+++67YvXu3uPLKK8WAAQM65ec/W2vPxw033CBeeuklsXPnTpGfny/mzJkjrFarOHbsWLDMzTffLKZPnx5yLVRUVHTUR2qT1p6PVatWCYvFEvJZS0tLQ8p0p+ujvLw85Fzs3btXKIoiVq1aFSyj5evjgw8+EA888IB45513BACxdu3aZst///33wmw2i4ULF4r9+/eLF154QSiKItavXx8s09pz3Nm09pz86le/Ek899ZT4+uuvxcGDB8XixYuFXq8XO3bsCJZZunSpGD58eMg1cvLkyTbFyWSmC9m/f78AIL755pvgtg8//FBIkiSOHz8e9nHeeust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hRdE3759hcFgEBdeeKH48ssvg/smTZokbr755pDyb731lhgyZIgwGAxi+PDh4t///nfIflVVxUMPPSR69eoljEajuPTSS8WBAwc64qNERWvOR79+/Rq9FpYuXSqEEMLpdIqpU6eKnj17Cr1eL/r16yfmzZunmS9mIVp3Pu65555g2V69eonLLrssZL4MIbrX9SGEEN99950AID7++OMGx9L69dHU92HgHNx8881i0qRJDV4zatQoYTAYxMCBA0Pm3Alo7hx3dq09J5MmTWq2vBD+4esZGRnCYDCI3r17i+uuu04UFBS0KU6umk1ERESaxj4zREREpGlMZoiIiEjTmMwQERGRpjGZISIiIk1jMkNERESaxmSGiIiINI3JDBEREWkakxkiIiJqtc8++wxXXHEFMjMzIUkS3n333VYfQwiBZ599FkOGDIHRaETv3r3xv//7v60+DpMZIurSNm/ejNzcXOj1esycORMbN26EJEmorKyMdWhB/fv3x4oVK2IdBlGrVFdXY+TIkXjppZciPsavfvUrvPrqq3j22Wfx3XffYd26dQ1WYg+HLuIIiIg0YOHChRg1ahQ+/PBDJCQkwGw2o6SkBFarNdahEWnajBkzMGPGjCb3u1wuPPDAA3jzzTdRWVmJc889F0899RQmT54MAMjPz8fKlSuxd+9eDB06FIB/8dZIsGaGiLq0wsJC/OhHP0KfPn2QlJQEg8GA9PR0SJLUaHmfzwdVVTs4SqKu584778TWrVuxZs0afPvtt/jpT3+K6dOn49ChQwCAf/3rXxg4cCDef/99DBgwAP3798fcuXNRUVHR6vdiMkPUzUyePBl33303fvOb3yA5ORnp6el45JFHgvsrKysxd+5c9OzZExaLBT/60Y+we/duAIDNZoOiKNi2bRsAQFVVJCcn46KLLgq+/q9//SuysrLCiuXYsWO4/vrrkZycjPj4eIwePRpfffVVcP/KlSuRnZ0Ng8GAoUOH4vXXXw95vSRJePXVVzFr1iyYzWYMHjwY69atAwD88MMPkCQJ5eXluPXWWyFJElavXt2gmWn16tVISkrCunXrcM4558BoNOLIkSPo378/Hn/8cdx0001ISEhAv379sG7dOpw8eRJXXXUVEhISMGLEiOC5CPjiiy8wYcIExMXFISsrC3fffTeqq6uD+8vKynDFFVcgLi4OAwYMwN/+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", + "image/png": "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4Vf+3XffFYMGDRIGg0EMGzZM/Pvf//bbr6qqePzxx0VKSoowGo3i6quvFkeOHOmIlxISrTkfffr0afJaWLJkiRBCCLvdLiZPnix69Ogh9Hq96NOnj5g7d65m3piFaN35ePDBB31lU1JSxDXXXOM3X4YQkXV9CCHEt99+KwCITz/9tNGxtH59NPd+WHcO7rjjDjFhwoRGj7n00kuFwWAQ/fv395tzp05L57iza+05mTBhQovlhfAOX09LSxMGg0H07NlT3HzzzSI/P79NcXLVbCIiItI09pkhIiIiTWMyQ0RERJrGZIaIiIg0jckMERERaRqTGSIiItI0JjNERESkaUxmiIiISNOYzBAREVGrff7557juuuuQnp4OSZLwwQcftPoYQgi88MILGDRoEIxGI3r27In//d//bfVxmMwQUZe2detWZGdnQ6/XY/r06di8eTMkSUJFRUW4Q/Pp27cvVqxYEe4wiFqlqqoKI0aMwKuvvhr0MX75y1/ijTfewAsvvIBvv/0W69evb7QSeyB0QUdARKQBCxYswKWXXoqPP/4YcXFxiImJQXFxMcxmc7hDI9K0adOmYdq0ac3udzgcePTRR/HOO++goqICl1xyCZ599llMnDgRAJCXl4eVK1fi4MGDGDx4MADv4q3BYM0MEXVpBQUF+OEPf4hevXohISEBBoMBqampkCSpyfIejweqqnZwlERdz3333Yft27djzZo12L9/P37yk59g6tSpOHbsGADgX//6F/r3748PP/wQ/fr1Q9++fTFnzhyUl5e3+rmYzBBFmIkTJ+KBBx7Ar3/9ayQmJiI1NRVPPvmkb39FRQXmzJmDHj16wGQy4Yc//CH27dsHALBYLFAUBTt37gQAqKqKxMREXHHFFb7H//Wvf0VGRkZAsZw+fRq33HILEhMTERsbi5EjR+Lrr7/27V+5ciUyMzNhMBgwePBgvPXWW36PlyQJb7zxBmbMmIGYmBgMHDgQ69evBwAcP34ckiShrKwMd911FyRJwurVqxs1M61evRoJCQlYv349hg4dCqPRiJMnT6Jv3774zW9+g9tvvx1xcXHo06cP1q9fj7Nnz+KGG25AXFwchg8f7jsXdb788kuMGzcO0dHRyMjIwAMPPICqqirf/tLSUlx33XWIjo5Gv3798Le//S2gc0WkJSdPnsSqVavw3nvvYdy4ccjMzMRD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", "text/plain": [ "
" ] @@ -692,7 +706,8 @@ "provenance": [] }, "kernelspec": { - "display_name": "Python 3", + "display_name": "venv", + "language": "python", "name": "python3" }, "language_info": { @@ -705,7 +720,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.12.6" } }, "nbformat": 4, From c3f51a2a8835c92e8f95bda315d18da20267890e Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Thu, 20 Mar 2025 15:57:50 -0700 Subject: [PATCH 10/39] chore: throws NotImplementedError when read_pandas with nested JSON type (#1516) * chore: throws NotImplementedError when read_pandas with nested JSON type * increase test coverage * fix python 3.6 --- bigframes/core/utils.py | 35 +++++++++++++++- tests/system/small/test_session.py | 64 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/bigframes/core/utils.py b/bigframes/core/utils.py index e38c43e73e..684290bf81 100644 --- a/bigframes/core/utils.py +++ b/bigframes/core/utils.py @@ -18,10 +18,12 @@ from typing import Hashable, Iterable, List import warnings +import bigframes_vendored.constants as constants import bigframes_vendored.pandas.io.common as vendored_pandas_io_common import numpy as np import pandas as pd import pandas.api.types as pdtypes +import pyarrow as pa import typing_extensions import bigframes.dtypes as dtypes @@ -243,6 +245,22 @@ def replace_timedeltas_with_micros(dataframe: pd.DataFrame) -> List[str]: return updated_columns +def _search_for_nested_json_type(arrow_type: pa.DataType) -> bool: + """ + Searches recursively for JSON array type within a PyArrow DataType. + """ + if arrow_type == dtypes.JSON_ARROW_TYPE: + return True + if pa.types.is_list(arrow_type): + return _search_for_nested_json_type(arrow_type.value_type) + if pa.types.is_struct(arrow_type): + for i in range(arrow_type.num_fields): + if _search_for_nested_json_type(arrow_type.field(i).type): + return True + return False + return False + + def replace_json_with_string(dataframe: pd.DataFrame) -> List[str]: """ Due to a BigQuery IO limitation with loading JSON from Parquet files (b/374784249), @@ -253,12 +271,27 @@ def replace_json_with_string(dataframe: pd.DataFrame) -> List[str]: updated_columns = [] for col in dataframe.columns: - if dataframe[col].dtype == dtypes.JSON_DTYPE: + column_type = dataframe[col].dtype + if column_type == dtypes.JSON_DTYPE: dataframe[col] = dataframe[col].astype(dtypes.STRING_DTYPE) updated_columns.append(col) + elif isinstance(column_type, pd.ArrowDtype) and _search_for_nested_json_type( + column_type.pyarrow_dtype + ): + raise NotImplementedError( + f"Nested JSON types, found in column `{col}`: `{column_type}`', " + f"are currently unsupported for upload. {constants.FEEDBACK_LINK}" + ) if dataframe.index.dtype == dtypes.JSON_DTYPE: dataframe.index = dataframe.index.astype(dtypes.STRING_DTYPE) updated_columns.append(dataframe.index.name) + elif isinstance( + dataframe.index.dtype, pd.ArrowDtype + ) and _search_for_nested_json_type(dataframe.index.dtype.pyarrow_dtype): + raise NotImplementedError( + f"Nested JSON types, found in the index: `{dataframe.index.dtype}`', " + f"are currently unsupported for upload. {constants.FEEDBACK_LINK}" + ) return updated_columns diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 323d002df4..663e5e2f10 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -26,6 +26,8 @@ import google.cloud.bigquery as bigquery import numpy as np import pandas as pd +import pandas.arrays as arrays +import pyarrow as pa import pytest import bigframes @@ -829,6 +831,68 @@ def test_read_pandas_json_index(session, write_engine): pd.testing.assert_index_equal(actual_result, expected_index) +@pytest.mark.parametrize( + ("write_engine"), + [ + pytest.param("default"), + pytest.param("bigquery_load"), + ], +) +def test_read_pandas_w_nested_json(session, write_engine): + data = [ + [{"json_field": "1"}], + [{"json_field": None}], + [{"json_field": '["1","3","5"]'}], + [{"json_field": '{"a":1,"b":["x","y"],"c":{"x":[],"z":false}}'}], + ] + # PyArrow currently lacks support for creating structs or lists containing extension types. + # See issue: https://github.com/apache/arrow/issues/45262 + pa_array = pa.array(data, type=pa.list_(pa.struct([("name", pa.string())]))) + pd_s = pd.Series( + arrays.ArrowExtensionArray(pa_array), # type: ignore + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("name", bigframes.dtypes.JSON_ARROW_TYPE)])) + ), + ) + with pytest.raises(NotImplementedError, match="Nested JSON types, found in column"): + # Until b/401630655 is resolved, json not compatible with allow_large_results=False + session.read_pandas(pd_s, write_engine=write_engine).to_pandas( + allow_large_results=True + ) + + +@pytest.mark.parametrize( + ("write_engine"), + [ + pytest.param("default"), + pytest.param("bigquery_load"), + ], +) +def test_read_pandas_w_nested_json_index(session, write_engine): + data = [ + [{"json_field": "1"}], + [{"json_field": None}], + [{"json_field": '["1","3","5"]'}], + [{"json_field": '{"a":1,"b":["x","y"],"c":{"x":[],"z":false}}'}], + ] + # PyArrow currently lacks support for creating structs or lists containing extension types. + # See issue: https://github.com/apache/arrow/issues/45262 + pa_array = pa.array(data, type=pa.list_(pa.struct([("name", pa.string())]))) + pd_idx: pd.Index = pd.Index( + arrays.ArrowExtensionArray(pa_array), # type: ignore + dtype=pd.ArrowDtype( + pa.list_(pa.struct([("name", bigframes.dtypes.JSON_ARROW_TYPE)])) + ), + ) + with pytest.raises( + NotImplementedError, match="Nested JSON types, found in the index" + ): + # Until b/401630655 is resolved, json not compatible with allow_large_results=False + session.read_pandas(pd_idx, write_engine=write_engine).to_pandas( + allow_large_results=True + ) + + @utils.skip_legacy_pandas @pytest.mark.parametrize( ("write_engine",), From 34ab9b8abd2c632c806afe69f00d9e7dddb6a8b5 Mon Sep 17 00:00:00 2001 From: Huan Chen <142538604+Genesis929@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:08:05 -0700 Subject: [PATCH 11/39] perf: update shape to use quer_and_wait (#1519) --- bigframes/session/executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index e539525d80..1bea5cec02 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -462,7 +462,7 @@ def get_row_count(self, array_value: bigframes.core.ArrayValue) -> int: generate_row_count_plan(array_value.node) ) sql = self.compiler.compile(row_count_plan, ordered=False) - iter, _ = self._run_execute_query(sql) + iter, _ = self._run_execute_query(sql, query_with_job=False) return next(iter)[0] def cached( From 08ea02cee2e713e64aec9ae4b4760af9e748adad Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Fri, 21 Mar 2025 11:23:41 -0700 Subject: [PATCH 12/39] refactor: Separate dataset id generation from temp table management (#1520) --- bigframes/blob/_functions.py | 4 ++- bigframes/dataframe.py | 7 +++-- bigframes/session/__init__.py | 10 +++---- bigframes/session/executor.py | 10 ++++--- bigframes/session/loader.py | 10 ++++--- bigframes/session/temp_storage.py | 39 ++++++++++++++------------- tests/system/large/test_session.py | 29 +++++++++++--------- tests/system/small/test_encryption.py | 4 +-- 8 files changed, 63 insertions(+), 50 deletions(-) diff --git a/bigframes/blob/_functions.py b/bigframes/blob/_functions.py index a3e7ae153c..6c9b435473 100644 --- a/bigframes/blob/_functions.py +++ b/bigframes/blob/_functions.py @@ -68,7 +68,9 @@ def _output_bq_type(self): def _create_udf(self): """Create Python UDF in BQ. Return name of the UDF.""" - udf_name = str(self._session._loader._storage_manager._random_table()) + udf_name = str( + self._session._loader._storage_manager.generate_unique_resource_id() + ) func_body = inspect.getsource(self._func) func_name = self._func.__name__ diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 4955bb1295..1d3a45e879 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -3760,10 +3760,9 @@ def to_gbq( ) if_exists = "replace" - temp_table_ref = self._session._temp_storage_manager._random_table( - # The client code owns this table reference now, so skip_cleanup=True - # to not clean it up when we close the session. - skip_cleanup=True, + # The client code owns this table reference now + temp_table_ref = ( + self._session._temp_storage_manager.generate_unique_resource_id() ) destination_table = f"{temp_table_ref.project}.{temp_table_ref.dataset_id}.{temp_table_ref.table_id}" diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index 7b416d4424..acaad4a5b7 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -248,7 +248,7 @@ def __init__( self._metrics = bigframes.session.metrics.ExecutionMetrics() self._function_session = bff_session.FunctionSession() self._temp_storage_manager = ( - bigframes.session.temp_storage.TemporaryGbqStorageManager( + bigframes.session.temp_storage.AnonymousDatasetManager( self._clients_provider.bqclient, location=self._location, session_id=self._session_id, @@ -908,7 +908,7 @@ def read_csv( engine=engine, write_engine=write_engine, ) - table = self._temp_storage_manager._random_table() + table = self._temp_storage_manager.allocate_temp_table() if engine is not None and engine == "bigquery": if any(param is not None for param in (dtype, names)): @@ -1054,7 +1054,7 @@ def read_parquet( engine=engine, write_engine=write_engine, ) - table = self._temp_storage_manager._random_table() + table = self._temp_storage_manager.allocate_temp_table() if engine == "bigquery": job_config = bigquery.LoadJobConfig() @@ -1108,7 +1108,7 @@ def read_json( engine=engine, write_engine=write_engine, ) - table = self._temp_storage_manager._random_table() + table = self._temp_storage_manager.allocate_temp_table() if engine == "bigquery": @@ -1704,7 +1704,7 @@ def _start_query_ml_ddl( def _create_object_table(self, path: str, connection: str) -> str: """Create a random id Object Table from the input path and connection.""" - table = str(self._loader._storage_manager._random_table()) + table = str(self._loader._storage_manager.generate_unique_resource_id()) import textwrap diff --git a/bigframes/session/executor.py b/bigframes/session/executor.py index 1bea5cec02..07645c2a98 100644 --- a/bigframes/session/executor.py +++ b/bigframes/session/executor.py @@ -195,7 +195,7 @@ class BigQueryCachingExecutor(Executor): def __init__( self, bqclient: bigquery.Client, - storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, + storage_manager: bigframes.session.temp_storage.AnonymousDatasetManager, bqstoragereadclient: google.cloud.bigquery_storage_v1.BigQueryReadClient, *, strictly_ordered: bool = True, @@ -248,7 +248,7 @@ def execute( job_config = bigquery.QueryJobConfig() # Use explicit destination to avoid 10GB limit of temporary table if use_explicit_destination: - destination_table = self.storage_manager.create_temp_table( + destination_table = self.storage_manager.allocate_and_create_temp_table( array_value.schema.to_bigquery(), cluster_cols=[] ) job_config.destination = destination_table @@ -392,7 +392,7 @@ def peek( job_config = bigquery.QueryJobConfig() # Use explicit destination to avoid 10GB limit of temporary table if use_explicit_destination: - destination_table = self.storage_manager.create_temp_table( + destination_table = self.storage_manager.allocate_and_create_temp_table( array_value.schema.to_bigquery(), cluster_cols=[] ) job_config.destination = destination_table @@ -645,7 +645,9 @@ def _sql_as_cached_temp_table( cluster_cols: Sequence[str], ) -> bigquery.TableReference: assert len(cluster_cols) <= _MAX_CLUSTER_COLUMNS - temp_table = self.storage_manager.create_temp_table(schema, cluster_cols) + temp_table = self.storage_manager.allocate_and_create_temp_table( + schema, cluster_cols + ) # TODO: Get default job config settings job_config = cast( diff --git a/bigframes/session/loader.py b/bigframes/session/loader.py index b9859e92a2..1296e9d1b3 100644 --- a/bigframes/session/loader.py +++ b/bigframes/session/loader.py @@ -115,7 +115,7 @@ def __init__( self, session: bigframes.session.Session, bqclient: bigquery.Client, - storage_manager: bigframes.session.temp_storage.TemporaryGbqStorageManager, + storage_manager: bigframes.session.temp_storage.AnonymousDatasetManager, default_index_type: bigframes.enums.DefaultIndexKind, scan_index_uniqueness: bool, force_total_order: bool, @@ -167,7 +167,7 @@ def read_pandas_load_job( job_config.labels = {"bigframes-api": api_name} - load_table_destination = self._storage_manager._random_table() + load_table_destination = self._storage_manager.allocate_temp_table() load_job = self._bqclient.load_table_from_dataframe( pandas_dataframe_copy, load_table_destination, @@ -216,7 +216,7 @@ def read_pandas_streaming( index=True, ) - destination = self._storage_manager.create_temp_table( + destination = self._storage_manager.allocate_and_create_temp_table( schema, [ordering_col], ) @@ -673,7 +673,9 @@ def _query_to_destination( ) else: cluster_cols = [] - temp_table = self._storage_manager.create_temp_table(schema, cluster_cols) + temp_table = self._storage_manager.allocate_and_create_temp_table( + schema, cluster_cols + ) timeout_ms = configuration.get("jobTimeoutMs") or configuration["query"].get( "timeoutMs" diff --git a/bigframes/session/temp_storage.py b/bigframes/session/temp_storage.py index de764e4535..3b2965efef 100644 --- a/bigframes/session/temp_storage.py +++ b/bigframes/session/temp_storage.py @@ -24,7 +24,7 @@ _TEMP_TABLE_ID_FORMAT = "bqdf{date}_{session_id}_{random_id}" -class TemporaryGbqStorageManager: +class AnonymousDatasetManager: """ Responsible for allocating and cleaning up temporary gbq tables used by a BigFrames session. """ @@ -46,20 +46,22 @@ def __init__( ) self.session_id = session_id - self._table_ids: List[str] = [] + self._table_ids: List[bigquery.TableReference] = [] self._kms_key = kms_key - def create_temp_table( + def allocate_and_create_temp_table( self, schema: Sequence[bigquery.SchemaField], cluster_cols: Sequence[str] ) -> bigquery.TableReference: - # Can't set a table in _SESSION as destination via query job API, so we - # run DDL, instead. + """ + Allocates and and creates a table in the anonymous dataset. + The table will be cleaned up by clean_up_tables. + """ expiration = ( datetime.datetime.now(datetime.timezone.utc) + constants.DEFAULT_EXPIRATION ) table = bf_io_bigquery.create_temp_table( self.bqclient, - self._random_table(), + self.allocate_temp_table(), expiration, schema=schema, cluster_columns=list(cluster_cols), @@ -67,11 +69,19 @@ def create_temp_table( ) return bigquery.TableReference.from_string(table) - def _random_table(self, skip_cleanup: bool = False) -> bigquery.TableReference: + def allocate_temp_table(self) -> bigquery.TableReference: + """ + Allocates a unique table id, but does not create the table. + The table will be cleaned up by clean_up_tables. + """ + table_id = self.generate_unique_resource_id() + self._table_ids.append(table_id) + return table_id + + def generate_unique_resource_id(self) -> bigquery.TableReference: """Generate a random table ID with BigQuery DataFrames prefix. - The generated ID will be stored and checked for deletion when the - session is closed, unless skip_cleanup is True. + This resource will not be cleaned up by this manager. Args: skip_cleanup (bool, default False): @@ -87,16 +97,9 @@ def _random_table(self, skip_cleanup: bool = False) -> bigquery.TableReference: table_id = _TEMP_TABLE_ID_FORMAT.format( date=now.strftime("%Y%m%d"), session_id=self.session_id, random_id=random_id ) - if not skip_cleanup: - self._table_ids.append(table_id) return self.dataset.table(table_id) def clean_up_tables(self): """Delete tables that were created with this session's session_id.""" - client = self.bqclient - project_id = self.dataset.project - dataset_id = self.dataset.dataset_id - - for table_id in self._table_ids: - full_id = ".".join([project_id, dataset_id, table_id]) - client.delete_table(full_id, not_found_ok=True) + for table_ref in self._table_ids: + self.bqclient.delete_table(table_ref, not_found_ok=True) diff --git a/tests/system/large/test_session.py b/tests/system/large/test_session.py index 7f13462cbe..e117cf0327 100644 --- a/tests/system/large/test_session.py +++ b/tests/system/large/test_session.py @@ -14,6 +14,7 @@ import datetime +import google.cloud.bigquery as bigquery import google.cloud.exceptions import pytest @@ -70,10 +71,14 @@ def test_close(session: bigframes.Session): + bigframes.constants.DEFAULT_EXPIRATION ) full_id_1 = bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration + session.bqclient, + session._temp_storage_manager.allocate_temp_table(), + expiration, ) full_id_2 = bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration + session.bqclient, + session._temp_storage_manager.allocate_temp_table(), + expiration, ) # check that the tables were actually created @@ -106,10 +111,14 @@ def test_clean_up_by_session_id(): + bigframes.constants.DEFAULT_EXPIRATION ) bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration + session.bqclient, + session._temp_storage_manager.allocate_temp_table(), + expiration, ) bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration + session.bqclient, + session._temp_storage_manager.allocate_temp_table(), + expiration, ) # check that some table exists with the expected session_id @@ -148,15 +157,11 @@ def test_clean_up_via_context_manager(session_creator): with session_creator() as session: bqclient = session.bqclient - expiration = ( - datetime.datetime.now(datetime.timezone.utc) - + bigframes.constants.DEFAULT_EXPIRATION + full_id_1 = session._temp_storage_manager.allocate_and_create_temp_table( + [bigquery.SchemaField("a", "INT64")], cluster_cols=[] ) - full_id_1 = bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration - ) - full_id_2 = bigframes.session._io.bigquery.create_temp_table( - session.bqclient, session._temp_storage_manager._random_table(), expiration + full_id_2 = session._temp_storage_manager.allocate_and_create_temp_table( + [bigquery.SchemaField("b", "STRING")], cluster_cols=["b"] ) # check that the tables were actually created diff --git a/tests/system/small/test_encryption.py b/tests/system/small/test_encryption.py index 72529bc5b0..8ce53c218b 100644 --- a/tests/system/small/test_encryption.py +++ b/tests/system/small/test_encryption.py @@ -89,7 +89,7 @@ def test_session_load_job(bq_cmek, session_with_bq_cmek): pytest.skip("no cmek set for testing") # pragma: NO COVER # Session should have cmek set in the default query and load job configs - load_table = session_with_bq_cmek._temp_storage_manager._random_table() + load_table = session_with_bq_cmek._temp_storage_manager.allocate_temp_table() df = pandas.DataFrame({"col0": [1, 2, 3]}) load_job_config = bigquery.LoadJobConfig() @@ -194,7 +194,7 @@ def test_to_gbq(bq_cmek, session_with_bq_cmek, scalars_table_id): # Write the result to BQ custom table and assert encryption session_with_bq_cmek.bqclient.get_table(output_table_id) - output_table_ref = session_with_bq_cmek._temp_storage_manager._random_table() + output_table_ref = session_with_bq_cmek._temp_storage_manager.allocate_temp_table() output_table_id = str(output_table_ref) df.to_gbq(output_table_id) output_table = session_with_bq_cmek.bqclient.get_table(output_table_id) From e9fe8154d83e2674a05d7b670e949368b175ec8b Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Fri, 21 Mar 2025 14:35:24 -0500 Subject: [PATCH 13/39] feat: add `GeoSeries.difference()` and `bigframes.bigquery.st_difference()` (#1471) * feat: add GeoSeries.difference() * add st_difference method and test cases for geo.difference and st_difference * update method and tests * update method and tests * update test to cover different inputs * update test assertion * update testing with single geometry object and add notebook tag to ignore the exception * fix docstrings * modify geo_difference to make it available for use and update tests and notebook * fix my py type error --- bigframes/bigquery/__init__.py | 3 +- bigframes/bigquery/_operations/geo.py | 120 ++++++ bigframes/core/compile/scalar_op_compiler.py | 17 +- bigframes/geopandas/geoseries.py | 5 +- bigframes/operations/__init__.py | 2 + bigframes/operations/geo_ops.py | 5 +- bigframes/operations/type.py | 14 + notebooks/geo/geoseries.ipynb | 386 ++++++++++++++---- tests/system/small/bigquery/test_geo.py | 96 ++++- .../system/small/geopandas/test_geoseries.py | 97 ++++- .../bigframes_vendored/geopandas/geoseries.py | 113 +++++ 11 files changed, 758 insertions(+), 100 deletions(-) diff --git a/bigframes/bigquery/__init__.py b/bigframes/bigquery/__init__.py index 56aee38bfe..a35e898c2d 100644 --- a/bigframes/bigquery/__init__.py +++ b/bigframes/bigquery/__init__.py @@ -27,7 +27,7 @@ unix_millis, unix_seconds, ) -from bigframes.bigquery._operations.geo import st_area +from bigframes.bigquery._operations.geo import st_area, st_difference from bigframes.bigquery._operations.json import ( json_extract, json_extract_array, @@ -48,6 +48,7 @@ "array_to_string", # geo ops "st_area", + "st_difference", # json ops "json_set", "json_extract", diff --git a/bigframes/bigquery/_operations/geo.py b/bigframes/bigquery/_operations/geo.py index 7b8e47e2da..a41c33f67d 100644 --- a/bigframes/bigquery/_operations/geo.py +++ b/bigframes/bigquery/_operations/geo.py @@ -15,6 +15,7 @@ from __future__ import annotations from bigframes import operations as ops +import bigframes.dtypes import bigframes.geopandas import bigframes.series @@ -91,3 +92,122 @@ def st_area(series: bigframes.series.Series) -> bigframes.series.Series: series = series._apply_unary_op(ops.geo_area_op) series.name = None return series + + +def st_difference( + series: bigframes.series.Series, other: bigframes.series.Series +) -> bigframes.series.Series: + """ + Returns a GEOGRAPHY that represents the point set difference of + `geography_1` and `geography_2`. Therefore, the result consists of the part + of `geography_1` that doesn't intersect with `geography_2`. + + If `geometry_1` is completely contained in `geometry_2`, then ST_DIFFERENCE + returns an empty GEOGRAPHY. + + ..note:: + BigQuery's Geography functions, like `st_difference`, interpret the geometry + data type as a point set on the Earth's surface. A point set is a set + of points, lines, and polygons on the WGS84 reference spheroid, with + geodesic edges. See: https://cloud.google.com/bigquery/docs/geospatial-data + + **Examples:** + + >>> import bigframes as bpd + >>> import bigframes.bigquery as bbq + >>> import bigframes.geopandas + >>> from shapely.geometry import Polygon, LineString, Point + >>> bpd.options.display.progress_bar = None + + We can check two GeoSeries against each other, row by row. + + >>> s1 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (2, 2), (0, 2)]), + ... Polygon([(0, 0), (2, 2), (0, 2)]), + ... LineString([(0, 0), (2, 2)]), + ... LineString([(2, 0), (0, 2)]), + ... Point(0, 1), + ... ], + ... ) + >>> s2 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (1, 1), (0, 1)]), + ... LineString([(1, 0), (1, 3)]), + ... LineString([(2, 0), (0, 2)]), + ... Point(1, 1), + ... Point(0, 1), + ... ], + ... index=range(1, 6), + ... ) + + >>> s1 + 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 1 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 2 LINESTRING (0 0, 2 2) + 3 LINESTRING (2 0, 0 2) + 4 POINT (0 1) + dtype: geometry + + >>> s2 + 1 POLYGON ((0 0, 1 1, 0 1, 0 0)) + 2 LINESTRING (1 0, 1 3) + 3 LINESTRING (2 0, 0 2) + 4 POINT (1 1) + 5 POINT (0 1) + dtype: geometry + + >>> bbq.st_difference(s1, s2) + 0 None + 1 POLYGON ((0.99954 1, 2 2, 0 2, 0 1, 0.99954 1)) + 2 LINESTRING (0 0, 1 1.00046, 2 2) + 3 GEOMETRYCOLLECTION EMPTY + 4 POINT (0 1) + 5 None + dtype: geometry + + We can also check difference of single shapely geometries: + + >>> sbq1 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]) + ... ] + ... ) + >>> sbq2 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]) + ... ] + ... ) + + >>> sbq1 + 0 POLYGON ((0 0, 10 0, 10 10, 0 0)) + dtype: geometry + + >>> sbq2 + 0 POLYGON ((4 2, 6 2, 8 6, 4 2)) + dtype: geometry + + >>> bbq.st_difference(sbq1, sbq2) + 0 POLYGON ((0 0, 10 0, 10 10, 0 0), (8 6, 6 2, 4... + dtype: geometry + + Additionally, we can check difference of a GeoSeries against a single shapely geometry: + + >>> bbq.st_difference(s1, sbq2) + 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 1 None + 2 None + 3 None + 4 None + dtype: geometry + + Args: + other (bigframes.series.Series or geometric object): + The GeoSeries (elementwise) or geometric object to find the difference to. + + Returns: + bigframes.series.Series: + A GeoSeries of the points in each aligned geometry that are not + in other. + """ + return series._apply_binary_op(other, ops.geo_st_difference_op) diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/scalar_op_compiler.py index 35a307722f..ce0cd6c37a 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/scalar_op_compiler.py @@ -1001,11 +1001,6 @@ def normalize_op_impl(x: ibis_types.Value): # Geo Ops -@scalar_op_compiler.register_unary_op(ops.geo_st_boundary_op, pass_op=False) -def geo_st_boundary_op_impl(x: ibis_types.Value): - return st_boundary(x) - - @scalar_op_compiler.register_unary_op(ops.geo_area_op) def geo_area_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.GeoSpatialValue, x).area() @@ -1016,6 +1011,18 @@ def geo_st_astext_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.GeoSpatialValue, x).as_text() +@scalar_op_compiler.register_unary_op(ops.geo_st_boundary_op, pass_op=False) +def geo_st_boundary_op_impl(x: ibis_types.Value): + return st_boundary(x) + + +@scalar_op_compiler.register_binary_op(ops.geo_st_difference_op, pass_op=False) +def geo_st_difference_op_impl(x: ibis_types.Value, y: ibis_types.Value): + return typing.cast(ibis_types.GeoSpatialValue, x).difference( + typing.cast(ibis_types.GeoSpatialValue, y) + ) + + @scalar_op_compiler.register_unary_op(ops.geo_st_geogfromtext_op) def geo_st_geogfromtext_op_impl(x: ibis_types.Value): # Ibis doesn't seem to provide a dedicated method to cast from string to geography, diff --git a/bigframes/geopandas/geoseries.py b/bigframes/geopandas/geoseries.py index 44018b8c5c..6c9cb77a08 100644 --- a/bigframes/geopandas/geoseries.py +++ b/bigframes/geopandas/geoseries.py @@ -62,7 +62,7 @@ def area(self, crs=None) -> bigframes.series.Series: # type: ignore Raises: NotImplementedError: - GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), insetead. + GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. """ raise NotImplementedError( f"GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. {constants.FEEDBACK_LINK}" @@ -93,3 +93,6 @@ def to_wkt(self: GeoSeries) -> bigframes.series.Series: series = self._apply_unary_op(ops.geo_st_astext_op) series.name = None return series + + def difference(self: GeoSeries, other: GeoSeries) -> bigframes.series.Series: # type: ignore + return self._apply_binary_op(other, ops.geo_st_difference_op) diff --git a/bigframes/operations/__init__.py b/bigframes/operations/__init__.py index 83cefbe6ba..2b4c9ca892 100644 --- a/bigframes/operations/__init__.py +++ b/bigframes/operations/__init__.py @@ -90,6 +90,7 @@ geo_area_op, geo_st_astext_op, geo_st_boundary_op, + geo_st_difference_op, geo_st_geogfromtext_op, geo_st_geogpoint_op, geo_x_op, @@ -366,6 +367,7 @@ # Geo ops "geo_area_op", "geo_st_boundary_op", + "geo_st_difference_op", "geo_st_astext_op", "geo_st_geogfromtext_op", "geo_st_geogpoint_op", diff --git a/bigframes/operations/geo_ops.py b/bigframes/operations/geo_ops.py index 9ef0983e24..3cf248bddb 100644 --- a/bigframes/operations/geo_ops.py +++ b/bigframes/operations/geo_ops.py @@ -37,6 +37,10 @@ ), ) +geo_st_difference_op = base_ops.create_binary_op( + name="geo_st_difference", type_signature=op_typing.BinaryGeo() +) + geo_st_geogfromtext_op = base_ops.create_unary_op( name="geo_st_geogfromtext", type_signature=op_typing.FixedOutputType( @@ -44,7 +48,6 @@ ), ) - geo_st_geogpoint_op = base_ops.create_binary_op( name="geo_st_geogpoint", type_signature=op_typing.BinaryNumericGeo() ) diff --git a/bigframes/operations/type.py b/bigframes/operations/type.py index 0a47cd91f0..b4029d74c7 100644 --- a/bigframes/operations/type.py +++ b/bigframes/operations/type.py @@ -122,6 +122,20 @@ def output_type( @dataclasses.dataclass +@dataclasses.dataclass +class BinaryGeo(BinaryTypeSignature): + """Type signature for geo functions like difference that can map geo to geo.""" + + def output_type( + self, left_type: ExpressionType, right_type: ExpressionType + ) -> ExpressionType: + if (left_type is not None) and not bigframes.dtypes.is_geo_like(left_type): + raise TypeError(f"Type {left_type} is not geo") + if (right_type is not None) and not bigframes.dtypes.is_geo_like(right_type): + raise TypeError(f"Type {right_type} is not numeric") + return bigframes.dtypes.GEO_DTYPE + + class BinaryNumericGeo(BinaryTypeSignature): """Type signature for geo functions like from_xy that can map ints to ints.""" diff --git a/notebooks/geo/geoseries.ipynb b/notebooks/geo/geoseries.ipynb index 7060128bf6..e7566f3fa6 100644 --- a/notebooks/geo/geoseries.ipynb +++ b/notebooks/geo/geoseries.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -49,14 +49,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/usr/local/google/home/arwas/src1/python-bigquery-dataframes/bigframes/session/_io/bigquery/read_gbq_table.py:280: DefaultIndexWarning: Table 'bigquery-public-data.geo_us_boundaries.counties' is clustered and/or partitioned, but BigQuery DataFrames was not able to find a suitable index. To avoid this warning, set at least one of: `index_col` or `filters`.\n", + "/usr/local/google/home/arwas/src1/python-bigquery-dataframes/bigframes/session/_io/bigquery/read_gbq_table.py:280: DefaultIndexWarning: \u001b[93mTable 'bigquery-public-data.geo_us_boundaries.counties' is clustered\n", + "and/or partitioned, but BigQuery DataFrames was not able to find a\n", + "suitable index. To avoid this warning, set at least one of:\n", + "`index_col` or `filters`.\u001b[0m\n", " warnings.warn(msg, category=bfe.DefaultIndexWarning)\n" ] } @@ -74,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -97,21 +100,21 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "137 POINT (-86.87338 38.37334)\n", - "164 POINT (-118.48037 46.25461)\n", - "333 POINT (-92.5617 32.30429)\n", - "703 POINT (-83.46189 39.55525)\n", - "846 POINT (-119.46779 47.21363)\n", + "217 POINT (-86.80185 38.70532)\n", + "16 POINT (-83.47042 30.44723)\n", + "40 POINT (-94.33925 38.25722)\n", + "139 POINT (-78.88532 38.50758)\n", + "400 POINT (-95.6191 41.0337)\n", "Name: int_point_geom, dtype: geometry" ] }, - "execution_count": 10, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -130,21 +133,21 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 POINT (-86.87338 38.37334)\n", - "1 POINT (-118.48037 46.25461)\n", - "2 POINT (-92.5617 32.30429)\n", - "3 POINT (-83.46189 39.55525)\n", - "4 POINT (-119.46779 47.21363)\n", + "0 POINT (-86.80185 38.70532)\n", + "1 POINT (-83.47042 30.44723)\n", + "2 POINT (-94.33925 38.25722)\n", + "3 POINT (-78.88532 38.50758)\n", + "4 POINT (-95.6191 41.0337)\n", "dtype: geometry" ] }, - "execution_count": 11, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -179,21 +182,21 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 -86.873385\n", - "1 -118.48037\n", - "2 -92.5617\n", - "3 -83.461893\n", - "4 -119.467788\n", + "0 -86.801847\n", + "1 -83.470416\n", + "2 -94.339246\n", + "3 -78.885321\n", + "4 -95.619101\n", "dtype: Float64" ] }, - "execution_count": 12, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -211,21 +214,21 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 38.373344\n", - "1 46.254606\n", - "2 32.30429\n", - "3 39.555246\n", - "4 47.213633\n", + "0 38.705322\n", + "1 30.447232\n", + "2 38.257217\n", + "3 38.507585\n", + "4 41.033703\n", "dtype: Float64" ] }, - "execution_count": 13, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -250,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -284,7 +287,7 @@ "dtype: Float64" ] }, - "execution_count": 14, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -302,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -336,7 +339,7 @@ "dtype: Float64" ] }, - "execution_count": 15, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -361,21 +364,21 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "78 POLYGON ((-95.97154 44.6306, -95.97919 44.6305...\n", - "130 POLYGON ((-95.0933 41.77694, -95.09331 41.7764...\n", - "544 POLYGON ((-96.0664 40.43618, -96.06639 40.4352...\n", - "995 POLYGON ((-101.83583 47.49547, -101.83665 47.4...\n", - "1036 POLYGON ((-88.42474 37.15094, -88.42526 37.149...\n", + "214 POLYGON ((-79.36704 34.96248, -79.36696 34.962...\n", + "161 POLYGON ((-89.08844 33.53252, -89.08843 33.532...\n", + "57 POLYGON ((-110.75069 35.50001, -110.75069 35.4...\n", + "46 POLYGON ((-94.6865 39.04405, -94.68764 39.0440...\n", + "260 POLYGON ((-100.53965 34.99391, -100.53966 34.9...\n", "Name: county_geom, dtype: geometry" ] }, - "execution_count": 16, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -394,21 +397,21 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 POLYGON ((-95.97154 44.6306, -95.97919 44.6305...\n", - "1 POLYGON ((-95.0933 41.77694, -95.09331 41.7764...\n", - "2 POLYGON ((-96.0664 40.43618, -96.06639 40.4352...\n", - "3 POLYGON ((-101.83583 47.49547, -101.83665 47.4...\n", - "4 POLYGON ((-88.42474 37.15094, -88.42526 37.149...\n", + "0 POLYGON ((-79.36704 34.96248, -79.36696 34.962...\n", + "1 POLYGON ((-89.08844 33.53252, -89.08843 33.532...\n", + "2 POLYGON ((-110.75069 35.50001, -110.75069 35.4...\n", + "3 POLYGON ((-94.6865 39.04405, -94.68764 39.0440...\n", + "4 POLYGON ((-100.53965 34.99391, -100.53966 34.9...\n", "dtype: geometry" ] }, - "execution_count": 17, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -433,7 +436,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 13, "metadata": { "tags": [ "raises-exception" @@ -442,14 +445,14 @@ "outputs": [ { "ename": "NotImplementedError", - "evalue": "GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. Share your usecase with the BigQuery DataFrames team at the https://bit.ly/bigframes-feedback survey.You are currently running BigFrames version 1.38.0", + "evalue": "GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. Share your usecase with the BigQuery DataFrames team at the https://bit.ly/bigframes-feedback survey. You are currently running BigFrames version 1.40.0.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNotImplementedError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[18], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfive_geom\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43marea\u001b[49m\n", - "File \u001b[0;32m~/src1/python-bigquery-dataframes/bigframes/geopandas/geoseries.py:67\u001b[0m, in \u001b[0;36mGeoSeries.area\u001b[0;34m(self, crs)\u001b[0m\n\u001b[1;32m 48\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[1;32m 49\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21marea\u001b[39m(\u001b[38;5;28mself\u001b[39m, crs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m bigframes\u001b[38;5;241m.\u001b[39mseries\u001b[38;5;241m.\u001b[39mSeries: \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 50\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Returns a Series containing the area of each geometry in the GeoSeries\u001b[39;00m\n\u001b[1;32m 51\u001b[0m \u001b[38;5;124;03m expressed in the units of the CRS.\u001b[39;00m\n\u001b[1;32m 52\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 65\u001b[0m \u001b[38;5;124;03m GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), insetead.\u001b[39;00m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 67\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 68\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mconstants\u001b[38;5;241m.\u001b[39mFEEDBACK_LINK\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 69\u001b[0m )\n", - "\u001b[0;31mNotImplementedError\u001b[0m: GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. Share your usecase with the BigQuery DataFrames team at the https://bit.ly/bigframes-feedback survey.You are currently running BigFrames version 1.38.0" + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfive_geom\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43marea\u001b[49m\n", + "File \u001b[0;32m~/src1/python-bigquery-dataframes/bigframes/geopandas/geoseries.py:67\u001b[0m, in \u001b[0;36mGeoSeries.area\u001b[0;34m(self, crs)\u001b[0m\n\u001b[1;32m 48\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[1;32m 49\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21marea\u001b[39m(\u001b[38;5;28mself\u001b[39m, crs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m bigframes\u001b[38;5;241m.\u001b[39mseries\u001b[38;5;241m.\u001b[39mSeries: \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 50\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Returns a Series containing the area of each geometry in the GeoSeries\u001b[39;00m\n\u001b[1;32m 51\u001b[0m \u001b[38;5;124;03m expressed in the units of the CRS.\u001b[39;00m\n\u001b[1;32m 52\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 65\u001b[0m \u001b[38;5;124;03m GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead.\u001b[39;00m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 67\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 68\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mconstants\u001b[38;5;241m.\u001b[39mFEEDBACK_LINK\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 69\u001b[0m )\n", + "\u001b[0;31mNotImplementedError\u001b[0m: GeoSeries.area is not supported. Use bigframes.bigquery.st_area(series), instead. Share your usecase with the BigQuery DataFrames team at the https://bit.ly/bigframes-feedback survey. You are currently running BigFrames version 1.40.0." ] } ], @@ -461,12 +464,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3. Use `bigframes.bigquery.st_area` to retirive the `area` in square meters instead. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_area" + "### 3. Use `bigframes.bigquery.st_area` to retrieve the `area` in square meters instead. See: https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_area" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -475,21 +478,21 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 1865212769.084914\n", - "1 1146753653.723439\n", - "2 1059653048.84506\n", - "3 2873655557.502374\n", - "4 886267772.361455\n", + "0 1014426111.476457\n", + "1 1196896004.730286\n", + "2 25794235993.165642\n", + "3 1242002056.351685\n", + "4 2381217221.963739\n", "dtype: Float64" ] }, - "execution_count": 20, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -515,21 +518,21 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 POINT (-86.87338 38.37334)\n", - "1 POINT (-118.48037 46.25461)\n", - "2 POINT (-92.5617 32.30429)\n", - "3 POINT (-83.46189 39.55525)\n", - "4 POINT (-119.46779 47.21363)\n", + "0 POINT (-86.80185 38.70532)\n", + "1 POINT (-83.47042 30.44723)\n", + "2 POINT (-94.33925 38.25722)\n", + "3 POINT (-78.88532 38.50758)\n", + "4 POINT (-95.6191 41.0337)\n", "dtype: geometry" ] }, - "execution_count": 21, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -554,21 +557,21 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 POINT(-86.8733845 38.3733441)\n", - "1 POINT(-118.4803697 46.2546057)\n", - "2 POINT(-92.5616997 32.3042901)\n", - "3 POINT(-83.4618927 39.5552462)\n", - "4 POINT(-119.467788 47.2136328)\n", + "0 POINT(-86.8018468 38.705322)\n", + "1 POINT(-83.4704159 30.4472325)\n", + "2 POINT(-94.3392459 38.2572171)\n", + "3 POINT(-78.8853213 38.5075848)\n", + "4 POINT(-95.619101 41.0337028)\n", "dtype: string" ] }, - "execution_count": 22, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -594,21 +597,21 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0 POINT (-86.87338 38.37334)\n", - "1 POINT (-118.48037 46.25461)\n", - "2 POINT (-92.5617 32.30429)\n", - "3 POINT (-83.46189 39.55525)\n", - "4 POINT (-119.46779 47.21363)\n", + "0 POINT (-86.80185 38.70532)\n", + "1 POINT (-83.47042 30.44723)\n", + "2 POINT (-94.33925 38.25722)\n", + "3 POINT (-78.88532 38.50758)\n", + "4 POINT (-95.6191 41.0337)\n", "dtype: geometry" ] }, - "execution_count": 23, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -627,7 +630,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -641,7 +644,7 @@ "dtype: geometry" ] }, - "execution_count": 24, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -662,7 +665,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -676,7 +679,7 @@ "dtype: geometry" ] }, - "execution_count": 25, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -684,6 +687,209 @@ "source": [ "geom_obj.geo.boundary" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Find the `difference` between two `GeoSeries` " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Reuse `wkts_from_geo` and `geom_obj` to find the difference between the geometry objects" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (-86.80185 38.70532)\n", + "1 POINT (-83.47042 30.44723)\n", + "2 GEOMETRYCOLLECTION EMPTY\n", + "3 POINT (-78.88532 38.50758)\n", + "4 POINT (-95.6191 41.0337)\n", + "dtype: geometry" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "wkts_from_geo.difference(geom_obj)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find the difference between a `GeoSeries` and a single geometry shape." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (-86.80185 38.70532)\n", + "1 None\n", + "2 None\n", + "3 None\n", + "4 None\n", + "dtype: geometry" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "wkts_from_geo.difference([Polygon([(0, 0), (10, 0), (10, 10), (0, 0)])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find the difference in `GeoSeries` with the same shapes" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 GEOMETRYCOLLECTION EMPTY\n", + "1 GEOMETRYCOLLECTION EMPTY\n", + "2 GEOMETRYCOLLECTION EMPTY\n", + "3 GEOMETRYCOLLECTION EMPTY\n", + "4 GEOMETRYCOLLECTION EMPTY\n", + "dtype: geometry" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "geom_obj.difference(geom_obj)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can also use`BigQuery.st_difference()` to find the difference between two `GeoSeries`. See, https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_difference" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (-86.80185 38.70532)\n", + "1 POINT (-83.47042 30.44723)\n", + "2 GEOMETRYCOLLECTION EMPTY\n", + "3 POINT (-78.88532 38.50758)\n", + "4 POINT (-95.6191 41.0337)\n", + "dtype: geometry" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.st_difference(wkts_from_geo, geom_obj)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find the difference between a `GeoSeries` and a single geometry shape." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 POINT (-86.80185 38.70532)\n", + "1 None\n", + "2 None\n", + "3 None\n", + "4 None\n", + "dtype: geometry" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.st_difference(wkts_from_geo, [Polygon([(0, 0), (10, 0), (10, 10), (0, 0)])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find the difference in GeoSeries with the same shapes" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 GEOMETRYCOLLECTION EMPTY\n", + "1 GEOMETRYCOLLECTION EMPTY\n", + "2 GEOMETRYCOLLECTION EMPTY\n", + "3 GEOMETRYCOLLECTION EMPTY\n", + "4 GEOMETRYCOLLECTION EMPTY\n", + "dtype: geometry" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bbq.st_difference(geom_obj, geom_obj)" + ] } ], "metadata": { diff --git a/tests/system/small/bigquery/test_geo.py b/tests/system/small/bigquery/test_geo.py index 7d38cd7d91..538099e80a 100644 --- a/tests/system/small/bigquery/test_geo.py +++ b/tests/system/small/bigquery/test_geo.py @@ -14,7 +14,12 @@ import geopandas # type: ignore import pandas as pd -from shapely.geometry import LineString, Point, Polygon # type: ignore +from shapely.geometry import ( # type: ignore + GeometryCollection, + LineString, + Point, + Polygon, +) import bigframes.bigquery as bbq import bigframes.geopandas @@ -51,3 +56,92 @@ def test_geo_st_area(): check_exact=False, rtol=1, ) + + +def test_geo_st_difference_with_geometry_objects(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + Point(0, 1), + ] + + data2 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + LineString([(2, 0), (0, 2)]), + ] + + geobf_s1 = bigframes.geopandas.GeoSeries(data=data1) + geobf_s2 = bigframes.geopandas.GeoSeries(data=data2) + geobf_s_result = bbq.st_difference(geobf_s1, geobf_s2).to_pandas() + + expected = bigframes.series.Series( + [ + GeometryCollection([]), + GeometryCollection([]), + Point(0, 1), + ], + index=[0, 1, 2], + dtype=geopandas.array.GeometryDtype(), + ).to_pandas() + + assert geobf_s_result.dtype == "geometry" + assert expected.iloc[0].equals(geobf_s_result.iloc[0]) + assert expected.iloc[1].equals(geobf_s_result.iloc[1]) + assert expected.iloc[2].equals(geobf_s_result.iloc[2]) + + +def test_geo_st_difference_with_single_geometry_object(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]), + Point(0, 1), + ] + + geobf_s1 = bigframes.geopandas.GeoSeries(data=data1) + geobf_s_result = bbq.st_difference( + geobf_s1, + bigframes.geopandas.GeoSeries( + [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(1, 0), (0, 5), (0, 0), (1, 0)]), + ] + ), + ).to_pandas() + + expected = bigframes.series.Series( + [ + GeometryCollection([]), + Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]), + None, + ], + index=[0, 1, 2], + dtype=geopandas.array.GeometryDtype(), + ).to_pandas() + + assert geobf_s_result.dtype == "geometry" + assert (expected.iloc[0]).equals(geobf_s_result.iloc[0]) + assert expected.iloc[1] == geobf_s_result.iloc[1] + assert expected.iloc[2] == geobf_s_result.iloc[2] + + +def test_geo_st_difference_with_similar_geometry_objects(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1)]), + Point(0, 1), + ] + + geobf_s1 = bigframes.geopandas.GeoSeries(data=data1) + geobf_s_result = bbq.st_difference(geobf_s1, geobf_s1).to_pandas() + + expected = bigframes.series.Series( + [GeometryCollection([]), GeometryCollection([]), GeometryCollection([])], + index=[0, 1, 2], + dtype=geopandas.array.GeometryDtype(), + ).to_pandas() + + assert geobf_s_result.dtype == "geometry" + assert expected.iloc[0].equals(geobf_s_result.iloc[0]) + assert expected.iloc[1].equals(geobf_s_result.iloc[1]) + assert expected.iloc[2].equals(geobf_s_result.iloc[2]) diff --git a/tests/system/small/geopandas/test_geoseries.py b/tests/system/small/geopandas/test_geoseries.py index d0987dbdaf..fdd9826468 100644 --- a/tests/system/small/geopandas/test_geoseries.py +++ b/tests/system/small/geopandas/test_geoseries.py @@ -20,7 +20,12 @@ import google.api_core.exceptions import pandas as pd import pytest -from shapely.geometry import LineString, Point, Polygon # type: ignore +from shapely.geometry import ( # type: ignore + GeometryCollection, + LineString, + Point, + Polygon, +) import bigframes.geopandas import bigframes.series @@ -194,3 +199,93 @@ def test_geo_boundary(): check_series_type=False, check_index=False, ) + + +# the GeoSeries and GeoPandas results are not always the same. +# For example, when the difference between two polygons is empty, +# GeoPandas returns 'POLYGON EMPTY' while GeoSeries returns 'GeometryCollection([])'. +# This is why we are hard-coding the expected results. +def test_geo_difference_with_geometry_objects(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + Point(0, 1), + ] + + data2 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + LineString([(2, 0), (0, 2)]), + ] + + bf_s1 = bigframes.geopandas.GeoSeries(data=data1) + bf_s2 = bigframes.geopandas.GeoSeries(data=data2) + + bf_result = bf_s1.difference(bf_s2).to_pandas() + + expected = bigframes.geopandas.GeoSeries( + [ + Polygon([]), + Polygon([]), + Point(0, 1), + ], + index=[0, 1, 2], + ).to_pandas() + + assert bf_result.dtype == "geometry" + assert expected.iloc[0].equals(bf_result.iloc[0]) + assert expected.iloc[1].equals(bf_result.iloc[1]) + assert expected.iloc[2].equals(bf_result.iloc[2]) + + +def test_geo_difference_with_single_geometry_object(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]), + Point(0, 1), + ] + + bf_s1 = bigframes.geopandas.GeoSeries(data=data1) + bf_result = bf_s1.difference( + bigframes.geopandas.GeoSeries( + [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(1, 0), (0, 5), (0, 0), (1, 0)]), + ] + ), + ).to_pandas() + + expected = bigframes.geopandas.GeoSeries( + [ + GeometryCollection([]), + Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]), + None, + ], + index=[0, 1, 2], + ).to_pandas() + + assert bf_result.dtype == "geometry" + assert (expected.iloc[0]).equals(bf_result.iloc[0]) + assert expected.iloc[1] == bf_result.iloc[1] + assert expected.iloc[2] == bf_result.iloc[2] + + +def test_geo_difference_with_similar_geometry_objects(): + data1 = [ + Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]), + Polygon([(0, 0), (1, 1), (0, 1)]), + Point(0, 1), + ] + + bf_s1 = bigframes.geopandas.GeoSeries(data=data1) + bf_result = bf_s1.difference(bf_s1).to_pandas() + + expected = bigframes.geopandas.GeoSeries( + [GeometryCollection([]), GeometryCollection([]), GeometryCollection([])], + index=[0, 1, 2], + ).to_pandas() + + assert bf_result.dtype == "geometry" + assert expected.iloc[0].equals(bf_result.iloc[0]) + assert expected.iloc[1].equals(bf_result.iloc[1]) + assert expected.iloc[2].equals(bf_result.iloc[2]) diff --git a/third_party/bigframes_vendored/geopandas/geoseries.py b/third_party/bigframes_vendored/geopandas/geoseries.py index a2e7b74059..b00d4220ff 100644 --- a/third_party/bigframes_vendored/geopandas/geoseries.py +++ b/third_party/bigframes_vendored/geopandas/geoseries.py @@ -239,3 +239,116 @@ def to_wkt(self) -> bigframes.series.Series: WKT representations of the geometries. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + def difference(self: GeoSeries, other: GeoSeries) -> GeoSeries: # type: ignore + """ + Returns a GeoSeries of the points in each aligned geometry that are not + in other. + + The operation works on a 1-to-1 row-wise manner + + **Examples:** + + >>> import bigframes as bpd + >>> import bigframes.geopandas + >>> from shapely.geometry import Polygon, LineString, Point + >>> bpd.options.display.progress_bar = None + + We can check two GeoSeries against each other, row by row. + + >>> s1 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (2, 2), (0, 2)]), + ... Polygon([(0, 0), (2, 2), (0, 2)]), + ... LineString([(0, 0), (2, 2)]), + ... LineString([(2, 0), (0, 2)]), + ... Point(0, 1), + ... ], + ... ) + >>> s2 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (1, 1), (0, 1)]), + ... LineString([(1, 0), (1, 3)]), + ... LineString([(2, 0), (0, 2)]), + ... Point(1, 1), + ... Point(0, 1), + ... ], + ... index=range(1, 6), + ... ) + + >>> s1 + 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 1 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 2 LINESTRING (0 0, 2 2) + 3 LINESTRING (2 0, 0 2) + 4 POINT (0 1) + dtype: geometry + + >>> s2 + 1 POLYGON ((0 0, 1 1, 0 1, 0 0)) + 2 LINESTRING (1 0, 1 3) + 3 LINESTRING (2 0, 0 2) + 4 POINT (1 1) + 5 POINT (0 1) + dtype: geometry + + >>> s1.difference(s2) + 0 None + 1 POLYGON ((0.99954 1, 2 2, 0 2, 0 1, 0.99954 1)) + 2 LINESTRING (0 0, 1 1.00046, 2 2) + 3 GEOMETRYCOLLECTION EMPTY + 4 POINT (0 1) + 5 None + dtype: geometry + + We can also check difference of single shapely geometries: + + >>> sbq1 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]) + ... ] + ... ) + >>> sbq2 = bigframes.geopandas.GeoSeries( + ... [ + ... Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]) + ... ] + ... ) + + >>> sbq1 + 0 POLYGON ((0 0, 10 0, 10 10, 0 0)) + dtype: geometry + + >>> sbq2 + 0 POLYGON ((4 2, 6 2, 8 6, 4 2)) + dtype: geometry + + >>> sbq1.difference(sbq2) + 0 POLYGON ((0 0, 10 0, 10 10, 0 0), (8 6, 6 2, 4... + dtype: geometry + + Additionally, we can check difference of a GeoSeries against a single shapely geometry: + + >>> s1.difference(sbq2) + 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) + 1 None + 2 None + 3 None + 4 None + dtype: geometry + + Args: + other (GeoSeries or geometric object): + The GeoSeries (elementwise) or geometric object to find the + difference to. + + Returns: + bigframes.geopandas.GeoSeries: + A GeoSeries of the points in each aligned geometry that are not + in other. + + Raises: + NotImplementedError: + GeoSeries.difference is not supported. Use + bigframes.bigquery.st_difference(series), instead. + """ + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From ab2455f7d6b3efea46ebf9a42836d6b8af09f6f2 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Fri, 21 Mar 2025 14:42:47 -0700 Subject: [PATCH 14/39] test: fix json tests fail locally with disable allow_large_results (#1523) * test: fix json tests fail locally with disable allow_large_results * fix test_read_gbq_w_json_in_array * move test_read_gbq_w_json* to test_session.py --- tests/system/small/bigquery/test_json.py | 91 +++++++------- tests/system/small/test_dataframe_io.py | 138 --------------------- tests/system/small/test_session.py | 149 +++++++++++++++++++++++ tests/unit/bigquery/__init__.py | 13 ++ tests/unit/bigquery/test_json.py | 26 ++++ 5 files changed, 238 insertions(+), 179 deletions(-) create mode 100644 tests/unit/bigquery/__init__.py create mode 100644 tests/unit/bigquery/test_json.py diff --git a/tests/system/small/bigquery/test_json.py b/tests/system/small/bigquery/test_json.py index 16f66dae57..57fc878643 100644 --- a/tests/system/small/bigquery/test_json.py +++ b/tests/system/small/bigquery/test_json.py @@ -22,13 +22,6 @@ import bigframes.pandas as bpd -@pytest.fixture(scope="module", autouse=True) -def use_large_query_path(): - # b/401630655 - with bpd.option_context("bigquery.allow_large_results", True): - yield - - @pytest.mark.parametrize( ("json_path", "expected_json"), [ @@ -39,12 +32,14 @@ def use_large_query_path(): def test_json_set_at_json_path(json_path, expected_json): original_json = ['{"a": {"b": {"c": "tester", "d": []}}}'] s = bpd.Series(original_json, dtype=dtypes.JSON_DTYPE) - actual = bbq.json_set(s, json_path_value_pairs=[(json_path, 10)]) + actual = bbq.json_set(s, json_path_value_pairs=[(json_path, 10)]) expected = bpd.Series(expected_json, dtype=dtypes.JSON_DTYPE) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -63,11 +58,12 @@ def test_json_set_at_json_value_type(json_value, expected_json): original_json = ['{"a": {"b": "dev"}}', '{"a": {"b": [1, 2]}}'] s = bpd.Series(original_json, dtype=dtypes.JSON_DTYPE) actual = bbq.json_set(s, json_path_value_pairs=[("$.a.b", json_value)]) - expected = bpd.Series(expected_json, dtype=dtypes.JSON_DTYPE) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -80,18 +76,14 @@ def test_json_set_w_more_pairs(): expected_json = ['{"a": 3, "b": 2}', '{"a": 4, "b": 2}', '{"a": 5, "b": 2, "c": 1}'] expected = bpd.Series(expected_json, dtype=dtypes.JSON_DTYPE) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) -def test_json_set_w_invalid_json_path_value_pairs(): - s = bpd.Series(['{"a": 10}'], dtype=dtypes.JSON_DTYPE) - with pytest.raises(ValueError): - bbq.json_set(s, json_path_value_pairs=[("$.a", 1, 100)]) # type: ignore - - def test_json_set_w_invalid_value_type(): s = bpd.Series(['{"a": 10}'], dtype=dtypes.JSON_DTYPE) with pytest.raises(TypeError): @@ -119,11 +111,13 @@ def test_json_extract_from_json(): ['{"a": {"b": [1, 2]}}', '{"a": {"c": 1}}', '{"a": {"b": 0}}'], dtype=dtypes.JSON_DTYPE, ) - actual = bbq.json_extract(s, "$.a.b").to_pandas() - expected = bpd.Series(["[1, 2]", None, "0"], dtype=dtypes.JSON_DTYPE).to_pandas() + actual = bbq.json_extract(s, "$.a.b") + expected = bpd.Series(["[1, 2]", None, "0"], dtype=dtypes.JSON_DTYPE) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual, - expected, + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -134,9 +128,11 @@ def test_json_extract_from_string(): ) actual = bbq.json_extract(s, "$.a.b") expected = bpd.Series(["[1,2]", None, "0"], dtype=pd.StringDtype(storage="pyarrow")) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -169,9 +165,10 @@ def test_json_extract_array_from_json(): expected.index.name = None expected.name = None + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -185,9 +182,11 @@ def test_json_extract_array_from_json_strings(): [['"ab"', '"2"', '"3 xy"'], [], ['"4"', '"5"'], None], dtype=pd.ArrowDtype(pa.list_(pa.string())), ) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -201,9 +200,11 @@ def test_json_extract_array_from_json_array_strings(): [["1", "2", "3"], [], ["4", "5"]], dtype=pd.ArrowDtype(pa.list_(pa.string())), ) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -217,9 +218,11 @@ def test_json_extract_string_array_from_json_strings(): s = bpd.Series(['{"a": ["ab", "2", "3 xy"]}', '{"a": []}', '{"a": ["4","5"]}']) actual = bbq.json_extract_string_array(s, "$.a") expected = bpd.Series([["ab", "2", "3 xy"], [], ["4", "5"]]) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -227,9 +230,11 @@ def test_json_extract_string_array_from_array_strings(): s = bpd.Series(["[1, 2, 3]", "[]", "[4,5]"]) actual = bbq.json_extract_string_array(s) expected = bpd.Series([["1", "2", "3"], [], ["4", "5"]]) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) @@ -237,17 +242,21 @@ def test_json_extract_string_array_as_float_array_from_array_strings(): s = bpd.Series(["[1, 2.5, 3]", "[]", "[4,5]"]) actual = bbq.json_extract_string_array(s, value_dtype=dtypes.FLOAT_DTYPE) expected = bpd.Series([[1, 2.5, 3], [], [4, 5]]) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False pd.testing.assert_series_equal( - actual.to_pandas(), - expected.to_pandas(), + actual.to_pandas(allow_large_results=True), + expected.to_pandas(allow_large_results=True), ) def test_json_extract_string_array_w_invalid_series_type(): + s = bpd.Series([1, 2]) with pytest.raises(TypeError): - bbq.json_extract_string_array(bpd.Series([1, 2])) + bbq.json_extract_string_array(s) def test_parse_json_w_invalid_series_type(): + s = bpd.Series([1, 2]) with pytest.raises(TypeError): - bbq.parse_json(bpd.Series([1, 2])) + bbq.parse_json(s) diff --git a/tests/system/small/test_dataframe_io.py b/tests/system/small/test_dataframe_io.py index d2ba96b41f..cd21f5094c 100644 --- a/tests/system/small/test_dataframe_io.py +++ b/tests/system/small/test_dataframe_io.py @@ -14,7 +14,6 @@ from typing import Tuple -import db_dtypes # type:ignore import google.api_core.exceptions import pandas as pd import pandas.testing @@ -281,143 +280,6 @@ def test_to_arrow_override_global_option(scalars_df_index): assert scalars_df_index._query_job.destination.table_id == table_id -def test_load_json_w_json_string_items(session): - sql = """ - SELECT 0 AS id, JSON_OBJECT('boolean', True) AS json_col, - UNION ALL - SELECT 1, JSON_OBJECT('int', 100), - UNION ALL - SELECT 2, JSON_OBJECT('float', 0.98), - UNION ALL - SELECT 3, JSON_OBJECT('string', 'hello world'), - UNION ALL - SELECT 4, JSON_OBJECT('array', [8, 9, 10]), - UNION ALL - SELECT 5, JSON_OBJECT('null', null), - UNION ALL - SELECT 6, JSON_OBJECT('b', 2, 'a', 1), - UNION ALL - SELECT - 7, - JSON_OBJECT( - 'dict', - JSON_OBJECT( - 'int', 1, - 'array', [JSON_OBJECT('foo', 1), JSON_OBJECT('bar', 'hello')] - ) - ), - """ - df = session.read_gbq(sql, index_col="id") - - assert df.dtypes["json_col"] == pd.ArrowDtype(db_dtypes.JSONArrowType()) - - assert df["json_col"][0] == '{"boolean":true}' - assert df["json_col"][1] == '{"int":100}' - assert df["json_col"][2] == '{"float":0.98}' - assert df["json_col"][3] == '{"string":"hello world"}' - assert df["json_col"][4] == '{"array":[8,9,10]}' - assert df["json_col"][5] == '{"null":null}' - - # Verifies JSON strings preserve array order, regardless of dictionary key order. - assert df["json_col"][6] == '{"a":1,"b":2}' - assert df["json_col"][7] == '{"dict":{"array":[{"foo":1},{"bar":"hello"}],"int":1}}' - - -def test_load_json_to_pandas_has_correct_result(session): - df = session.read_gbq("SELECT JSON_OBJECT('foo', 10, 'bar', TRUE) AS json_col") - assert df.dtypes["json_col"] == pd.ArrowDtype(db_dtypes.JSONArrowType()) - result = df.to_pandas() - - # These JSON strings are compatible with BigQuery's JSON storage, - pd_df = pd.DataFrame( - {"json_col": ['{"bar":true,"foo":10}']}, - dtype=pd.ArrowDtype(db_dtypes.JSONArrowType()), - ) - pd_df.index = pd_df.index.astype("Int64") - pd.testing.assert_series_equal(result.dtypes, pd_df.dtypes) - pd.testing.assert_series_equal(result["json_col"], pd_df["json_col"]) - - -def test_load_json_in_struct(session): - """Avoid regressions for internal issue 381148539.""" - sql = """ - SELECT 0 AS id, STRUCT(JSON_OBJECT('boolean', True) AS data, 1 AS number) AS struct_col - UNION ALL - SELECT 1, STRUCT(JSON_OBJECT('int', 100), 2), - UNION ALL - SELECT 2, STRUCT(JSON_OBJECT('float', 0.98), 3), - UNION ALL - SELECT 3, STRUCT(JSON_OBJECT('string', 'hello world'), 4), - UNION ALL - SELECT 4, STRUCT(JSON_OBJECT('array', [8, 9, 10]), 5), - UNION ALL - SELECT 5, STRUCT(JSON_OBJECT('null', null), 6), - UNION ALL - SELECT - 6, - STRUCT(JSON_OBJECT( - 'dict', - JSON_OBJECT( - 'int', 1, - 'array', [JSON_OBJECT('foo', 1), JSON_OBJECT('bar', 'hello')] - ) - ), 7), - """ - df = session.read_gbq(sql, index_col="id") - - assert isinstance(df.dtypes["struct_col"], pd.ArrowDtype) - assert isinstance(df.dtypes["struct_col"].pyarrow_dtype, pa.StructType) - - data = df["struct_col"].struct.field("data") - assert data.dtype == pd.ArrowDtype(db_dtypes.JSONArrowType()) - - assert data[0] == '{"boolean":true}' - assert data[1] == '{"int":100}' - assert data[2] == '{"float":0.98}' - assert data[3] == '{"string":"hello world"}' - assert data[4] == '{"array":[8,9,10]}' - assert data[5] == '{"null":null}' - assert data[6] == '{"dict":{"array":[{"foo":1},{"bar":"hello"}],"int":1}}' - - -def test_load_json_in_array(session): - sql = """ - SELECT - 0 AS id, - [ - JSON_OBJECT('boolean', True), - JSON_OBJECT('int', 100), - JSON_OBJECT('float', 0.98), - JSON_OBJECT('string', 'hello world'), - JSON_OBJECT('array', [8, 9, 10]), - JSON_OBJECT('null', null), - JSON_OBJECT( - 'dict', - JSON_OBJECT( - 'int', 1, - 'array', [JSON_OBJECT('bar', 'hello'), JSON_OBJECT('foo', 1)] - ) - ) - ] AS array_col, - """ - df = session.read_gbq(sql, index_col="id") - - assert isinstance(df.dtypes["array_col"], pd.ArrowDtype) - assert isinstance(df.dtypes["array_col"].pyarrow_dtype, pa.ListType) - - data = df["array_col"].list - assert data.len()[0] == 7 - assert data[0].dtype == pd.ArrowDtype(db_dtypes.JSONArrowType()) - - assert data[0][0] == '{"boolean":true}' - assert data[1][0] == '{"int":100}' - assert data[2][0] == '{"float":0.98}' - assert data[3][0] == '{"string":"hello world"}' - assert data[4][0] == '{"array":[8,9,10]}' - assert data[5][0] == '{"null":null}' - assert data[6][0] == '{"dict":{"array":[{"bar":"hello"},{"foo":1}],"int":1}}' - - def test_to_pandas_batches_w_correct_dtypes(scalars_df_default_index): """Verify to_pandas_batches() APIs returns the expected dtypes.""" expected = scalars_df_default_index.dtypes diff --git a/tests/system/small/test_session.py b/tests/system/small/test_session.py index 663e5e2f10..e286c40450 100644 --- a/tests/system/small/test_session.py +++ b/tests/system/small/test_session.py @@ -22,6 +22,7 @@ import warnings import bigframes_vendored.pandas.io.gbq as vendored_pandas_gbq +import db_dtypes # type:ignore import google import google.cloud.bigquery as bigquery import numpy as np @@ -603,6 +604,154 @@ def test_read_gbq_external_table(session: bigframes.Session): assert df["i1"].max() == 99 +def test_read_gbq_w_json(session): + sql = """ + SELECT 0 AS id, JSON_OBJECT('boolean', True) AS json_col, + UNION ALL + SELECT 1, JSON_OBJECT('int', 100), + UNION ALL + SELECT 2, JSON_OBJECT('float', 0.98), + UNION ALL + SELECT 3, JSON_OBJECT('string', 'hello world'), + UNION ALL + SELECT 4, JSON_OBJECT('array', [8, 9, 10]), + UNION ALL + SELECT 5, JSON_OBJECT('null', null), + UNION ALL + SELECT 6, JSON_OBJECT('b', 2, 'a', 1), + UNION ALL + SELECT + 7, + JSON_OBJECT( + 'dict', + JSON_OBJECT( + 'int', 1, + 'array', [JSON_OBJECT('foo', 1), JSON_OBJECT('bar', 'hello')] + ) + ), + """ + # TODO(b/401630655): JSON is not compatible with allow_large_results=False + df = session.read_gbq(sql, index_col="id").to_pandas(allow_large_results=True) + + assert df.dtypes["json_col"] == pd.ArrowDtype(db_dtypes.JSONArrowType()) + + assert df["json_col"][0] == '{"boolean":true}' + assert df["json_col"][1] == '{"int":100}' + assert df["json_col"][2] == '{"float":0.98}' + assert df["json_col"][3] == '{"string":"hello world"}' + assert df["json_col"][4] == '{"array":[8,9,10]}' + assert df["json_col"][5] == '{"null":null}' + + # Verifies JSON strings preserve array order, regardless of dictionary key order. + assert df["json_col"][6] == '{"a":1,"b":2}' + assert df["json_col"][7] == '{"dict":{"array":[{"foo":1},{"bar":"hello"}],"int":1}}' + + +def test_read_gbq_w_json_and_compare_w_pandas_json(session): + df = session.read_gbq("SELECT JSON_OBJECT('foo', 10, 'bar', TRUE) AS json_col") + assert df.dtypes["json_col"] == pd.ArrowDtype(db_dtypes.JSONArrowType()) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False + result = df.to_pandas(allow_large_results=True) + + # These JSON strings are compatible with BigQuery's JSON storage, + pd_df = pd.DataFrame( + {"json_col": ['{"bar":true,"foo":10}']}, + dtype=pd.ArrowDtype(db_dtypes.JSONArrowType()), + ) + pd_df.index = pd_df.index.astype("Int64") + pd.testing.assert_series_equal(result.dtypes, pd_df.dtypes) + pd.testing.assert_series_equal(result["json_col"], pd_df["json_col"]) + + +def test_read_gbq_w_json_in_struct(session): + """Avoid regressions for internal issue 381148539.""" + sql = """ + SELECT 0 AS id, STRUCT(JSON_OBJECT('boolean', True) AS data, 1 AS number) AS struct_col + UNION ALL + SELECT 1, STRUCT(JSON_OBJECT('int', 100), 2), + UNION ALL + SELECT 2, STRUCT(JSON_OBJECT('float', 0.98), 3), + UNION ALL + SELECT 3, STRUCT(JSON_OBJECT('string', 'hello world'), 4), + UNION ALL + SELECT 4, STRUCT(JSON_OBJECT('array', [8, 9, 10]), 5), + UNION ALL + SELECT 5, STRUCT(JSON_OBJECT('null', null), 6), + UNION ALL + SELECT + 6, + STRUCT(JSON_OBJECT( + 'dict', + JSON_OBJECT( + 'int', 1, + 'array', [JSON_OBJECT('foo', 1), JSON_OBJECT('bar', 'hello')] + ) + ), 7), + """ + df = session.read_gbq(sql, index_col="id") + + assert isinstance(df.dtypes["struct_col"], pd.ArrowDtype) + assert isinstance(df.dtypes["struct_col"].pyarrow_dtype, pa.StructType) + + data = df["struct_col"].struct.field("data") + assert data.dtype == pd.ArrowDtype(db_dtypes.JSONArrowType()) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False + data = data.to_pandas(allow_large_results=True) + + assert data[0] == '{"boolean":true}' + assert data[1] == '{"int":100}' + assert data[2] == '{"float":0.98}' + assert data[3] == '{"string":"hello world"}' + assert data[4] == '{"array":[8,9,10]}' + assert data[5] == '{"null":null}' + assert data[6] == '{"dict":{"array":[{"foo":1},{"bar":"hello"}],"int":1}}' + + +def test_read_gbq_w_json_in_array(session): + sql = """ + SELECT + 0 AS id, + [ + JSON_OBJECT('boolean', True), + JSON_OBJECT('int', 100), + JSON_OBJECT('float', 0.98), + JSON_OBJECT('string', 'hello world'), + JSON_OBJECT('array', [8, 9, 10]), + JSON_OBJECT('null', null), + JSON_OBJECT( + 'dict', + JSON_OBJECT( + 'int', 1, + 'array', [JSON_OBJECT('bar', 'hello'), JSON_OBJECT('foo', 1)] + ) + ) + ] AS array_col, + """ + df = session.read_gbq(sql, index_col="id") + + assert isinstance(df.dtypes["array_col"], pd.ArrowDtype) + assert isinstance(df.dtypes["array_col"].pyarrow_dtype, pa.ListType) + + data = df["array_col"] + assert data.list.len()[0] == 7 + assert data.list[0].dtype == pd.ArrowDtype(db_dtypes.JSONArrowType()) + + # TODO(b/401630655): JSON is not compatible with allow_large_results=False + pd_data = data.to_pandas(allow_large_results=True) + + assert pd_data[0] == [ + '{"boolean":true}', + '{"int":100}', + '{"float":0.98}', + '{"string":"hello world"}', + '{"array":[8,9,10]}', + '{"null":null}', + '{"dict":{"array":[{"bar":"hello"},{"foo":1}],"int":1}}', + ] + + def test_read_gbq_model(session, penguins_linear_model_name): model = session.read_gbq_model(penguins_linear_model_name) assert isinstance(model, bigframes.ml.linear_model.LinearRegression) diff --git a/tests/unit/bigquery/__init__.py b/tests/unit/bigquery/__init__.py new file mode 100644 index 0000000000..0a2669d7a2 --- /dev/null +++ b/tests/unit/bigquery/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/tests/unit/bigquery/test_json.py b/tests/unit/bigquery/test_json.py new file mode 100644 index 0000000000..d9beea26db --- /dev/null +++ b/tests/unit/bigquery/test_json.py @@ -0,0 +1,26 @@ +# 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 unittest.mock as mock + +import pytest + +import bigframes.bigquery as bbq +import bigframes.pandas as bpd + + +def test_json_set_w_invalid_json_path_value_pairs(): + mock_series = mock.create_autospec(bpd.pandas.Series, instance=True) + with pytest.raises(ValueError, match="Incorrect format"): + bbq.json_set(mock_series, json_path_value_pairs=[("$.a", 1, 100)]) # type: ignore From 499e00a0305fd63ebdedf30c06b775903951ac96 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Mon, 24 Mar 2025 09:51:53 -0700 Subject: [PATCH 15/39] chore: ensure udfs are actively deleted, make pre-commit mypy dep consistent (#1522) --- .pre-commit-config.yaml | 2 +- .../large/functions/test_managed_function.py | 28 +++++++++++-------- tests/system/utils.py | 18 +++++++++++- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d11c951a1..8ca120bd07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,6 @@ repos: rev: v1.10.0 hooks: - id: mypy - additional_dependencies: [types-requests, types-tabulate, pandas-stubs] + additional_dependencies: [types-requests, types-tabulate, pandas-stubs<=2.2.3.241126] exclude: "^third_party" args: ["--check-untyped-defs", "--explicit-package-bases", "--ignore-missing-imports"] diff --git a/tests/system/large/functions/test_managed_function.py b/tests/system/large/functions/test_managed_function.py index 47cbf7fb1b..7c8c74e005 100644 --- a/tests/system/large/functions/test_managed_function.py +++ b/tests/system/large/functions/test_managed_function.py @@ -72,7 +72,7 @@ def multiply(x, y): ) finally: # clean up the gcp assets created for the managed function. - cleanup_function_assets(multiply, bigquery_client) + cleanup_function_assets(multiply, bigquery_client, ignore_failures=False) def test_managed_function_stringify_with_ibis( @@ -118,7 +118,7 @@ def stringify(x): ) finally: # clean up the gcp assets created for the managed function. - cleanup_function_assets(stringify, bigquery_client) + cleanup_function_assets(stringify, bigquery_client, ignore_failures=False) @pytest.mark.parametrize( @@ -167,7 +167,7 @@ def featurize(x: int) -> list[array_dtype]: # type: ignore finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(featurize, session.bqclient) + cleanup_function_assets(featurize, session.bqclient, ignore_failures=False) @pytest.mark.parametrize( @@ -234,7 +234,7 @@ def foo(x: int) -> typ: # type:ignore pandas.testing.assert_frame_equal(bf_result_gbq, pd_result, check_dtype=False) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(foo, session.bqclient) + cleanup_function_assets(foo, session.bqclient, ignore_failures=False) @pytest.mark.parametrize( @@ -274,7 +274,7 @@ def foo_list(x: int) -> list[typ]: # type:ignore pandas.testing.assert_frame_equal(bf_result, pd_result, check_dtype=False) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(foo_list, session.bqclient) + cleanup_function_assets(foo_list, session.bqclient, ignore_failures=False) def test_managed_function_series_combine(session, scalars_dfs): @@ -330,7 +330,9 @@ def add(x: int, y: int) -> int: pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(add_managed_func, session.bqclient) + cleanup_function_assets( + add_managed_func, session.bqclient, ignore_failures=False + ) def test_managed_function_series_combine_array_output(session, scalars_dfs): @@ -391,7 +393,9 @@ def add_list(x: int, y: int) -> list[int]: pandas.testing.assert_series_equal(bf_result_gbq, pd_result, check_dtype=False) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(add_list_managed_func, session.bqclient) + cleanup_function_assets( + add_list_managed_func, session.bqclient, ignore_failures=False + ) def test_managed_function_dataframe_map(session, scalars_dfs): @@ -425,7 +429,7 @@ def add_one(x): pandas.testing.assert_frame_equal(bf_result, pd_result) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(mf_add_one, session.bqclient) + cleanup_function_assets(mf_add_one, session.bqclient, ignore_failures=False) def test_managed_function_dataframe_map_array_output( @@ -464,7 +468,9 @@ def add_one_list(x): pandas.testing.assert_frame_equal(bf_result_gbq, pd_result, check_dtype=False) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(mf_add_one_list, session.bqclient) + cleanup_function_assets( + mf_add_one_list, session.bqclient, ignore_failures=False + ) def test_managed_function_dataframe_apply_axis_1(session, scalars_dfs): @@ -500,7 +506,7 @@ def add_ints(x, y): ) finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(add_ints_mf, session.bqclient) + cleanup_function_assets(add_ints_mf, session.bqclient, ignore_failures=False) def test_managed_function_dataframe_apply_axis_1_array_output(session): @@ -605,4 +611,4 @@ def foo(x, y, z): finally: # Clean up the gcp assets created for the managed function. - cleanup_function_assets(foo, session.bqclient) + cleanup_function_assets(foo, session.bqclient, ignore_failures=False) diff --git a/tests/system/utils.py b/tests/system/utils.py index bc1fe6745e..891d813935 100644 --- a/tests/system/utils.py +++ b/tests/system/utils.py @@ -401,7 +401,7 @@ def cleanup_function_assets( ) -> None: """Clean up the GCP assets behind a bigframess function.""" - # Clean up bigframes function. + # Clean up bigframes bigquery function. try: bigquery_client.delete_routine(bigframes_func.bigframes_bigquery_function) except Exception: @@ -409,6 +409,12 @@ def cleanup_function_assets( if not ignore_failures: raise + if not ignore_failures: + # Make sure that the BQ routins is actually deleted + with pytest.raises(google.api_core.exceptions.NotFound): + bigquery_client.get_routine(bigframes_func.bigframes_bigquery_function) + + # Clean up bigframes cloud run function if cloudfunctions_client: # Clean up cloud function try: @@ -420,6 +426,16 @@ def cleanup_function_assets( if not ignore_failures: raise + if not ignore_failures: + # Make sure the cloud run function is actually deleted + try: + gcf = cloudfunctions_client.get_function( + name=bigframes_func.bigframes_cloud_function + ) + assert gcf.state is functions_v2.Function.State.DELETING + except google.cloud.exceptions.NotFound: + pass + def get_function_name(func, package_requirements=None, is_row_processor=False): """Get a bigframes function name for testing given a udf.""" From 578081e978f2cca21ddae8b3ee371972ba723777 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Mon, 24 Mar 2025 10:41:45 -0700 Subject: [PATCH 16/39] fix: read_pandas inline returns None when exceeds limit (#1525) --- bigframes/session/__init__.py | 42 ++++++++++++++---------------- tests/unit/session/test_session.py | 33 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index acaad4a5b7..dfee41c90b 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -794,13 +794,14 @@ def _read_pandas( ) if write_engine == "default": - inline_df = self._read_pandas_inline(pandas_dataframe, should_raise=False) - if inline_df is not None: + try: + inline_df = self._read_pandas_inline(pandas_dataframe) return inline_df + except ValueError: + pass return self._read_pandas_load_job(pandas_dataframe, api_name) elif write_engine == "bigquery_inline": - # Regarding the type: ignore, with should_raise=True, this should never return None. - return self._read_pandas_inline(pandas_dataframe, should_raise=True) # type: ignore + return self._read_pandas_inline(pandas_dataframe) elif write_engine == "bigquery_load": return self._read_pandas_load_job(pandas_dataframe, api_name) elif write_engine == "bigquery_streaming": @@ -809,12 +810,16 @@ def _read_pandas( raise ValueError(f"Got unexpected write_engine '{write_engine}'") def _read_pandas_inline( - self, pandas_dataframe: pandas.DataFrame, should_raise=False - ) -> Optional[dataframe.DataFrame]: + self, pandas_dataframe: pandas.DataFrame + ) -> dataframe.DataFrame: import bigframes.dataframe as dataframe - if pandas_dataframe.memory_usage(deep=True).sum() > MAX_INLINE_DF_BYTES: - return None + memory_usage = pandas_dataframe.memory_usage(deep=True).sum() + if memory_usage > MAX_INLINE_DF_BYTES: + raise ValueError( + f"DataFrame size ({memory_usage} bytes) exceeds the maximum allowed " + f"for inline data ({MAX_INLINE_DF_BYTES} bytes)." + ) try: local_block = blocks.Block.from_local(pandas_dataframe, self) @@ -825,29 +830,22 @@ def _read_pandas_inline( ValueError, # Thrown by ibis for some unhandled types TypeError, # Not all types handleable by local code path ) as exc: - if should_raise: - raise ValueError( - f"Could not convert with a BigQuery type: `{exc}`. " - ) from exc - else: - return None - - inline_types = inline_df._block.expr.schema.dtypes + raise ValueError( + f"Could not convert with a BigQuery type: `{exc}`. " + ) from exc # Make sure all types are inlinable to avoid escaping errors. + inline_types = inline_df._block.expr.schema.dtypes noninlinable_types = [ dtype for dtype in inline_types if dtype not in INLINABLE_DTYPES ] - if len(noninlinable_types) == 0: - return inline_df - - if should_raise: + if len(noninlinable_types) != 0: raise ValueError( f"Could not inline with a BigQuery type: `{noninlinable_types}`. " f"{constants.FEEDBACK_LINK}" ) - else: - return None + + return inline_df def _read_pandas_load_job( self, diff --git a/tests/unit/session/test_session.py b/tests/unit/session/test_session.py index d024d332d4..b35449f291 100644 --- a/tests/unit/session/test_session.py +++ b/tests/unit/session/test_session.py @@ -22,6 +22,8 @@ import google.api_core.exceptions import google.cloud.bigquery import google.cloud.bigquery.table +import pandas as pd +import pyarrow as pa import pytest import bigframes @@ -458,3 +460,34 @@ def today(cls): with pytest.warns(bigframes.exceptions.ObsoleteVersionWarning): resources.create_bigquery_session() + + +@mock.patch("bigframes.session.MAX_INLINE_DF_BYTES", 1) +def test_read_pandas_inline_exceeds_limit_raises_error(): + session = resources.create_bigquery_session() + pd_df = pd.DataFrame([[1, 2, 3], [4, 5, 6]]) + with pytest.raises( + ValueError, + match=r"DataFrame size \(.* bytes\) exceeds the maximum allowed for inline data \(1 bytes\)\.", + ): + session.read_pandas(pd_df, write_engine="bigquery_inline") + + +def test_read_pandas_inline_w_interval_type_raises_error(): + session = resources.create_bigquery_session() + df = pd.DataFrame(pd.arrays.IntervalArray.from_breaks([0, 10, 20, 30, 40, 50])) + with pytest.raises(ValueError, match="Could not convert with a BigQuery type: "): + session.read_pandas(df, write_engine="bigquery_inline") + + +def test_read_pandas_inline_w_noninlineable_type_raises_error(): + session = resources.create_bigquery_session() + data = [ + [1, 2, 3], + [4, 5], + None, + [6, 7, 8, 9], + ] + s = pd.Series(data, dtype=pd.ArrowDtype(pa.list_(pa.int64()))) + with pytest.raises(ValueError, match="Could not inline with a BigQuery type:"): + session.read_pandas(s, write_engine="bigquery_inline") From 9ff3fa8bfc933ff407f449b84e8352bdf2debc47 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 25 Mar 2025 07:47:33 -0700 Subject: [PATCH 17/39] chore: disable managed function tests temporarily (#1532) This is to give a breather to the BigQuery managed udf team to troubleshoot the quota issue. --- tests/system/large/blob/test_function.py | 6 ++++++ tests/system/large/functions/test_managed_function.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/system/large/blob/test_function.py b/tests/system/large/blob/test_function.py index dfdbbffede..588d9b3321 100644 --- a/tests/system/large/blob/test_function.py +++ b/tests/system/large/blob/test_function.py @@ -25,6 +25,12 @@ from bigframes import dtypes import bigframes.pandas as bpd +# TODO(shobs): restore these tests after the managed udf cleanup issue is +# resolved in the test project +pytestmark = pytest.mark.skip( + reason="temporarily disable to debug managed udf cleanup in the test project" +) + @pytest.fixture(scope="function") def images_output_folder() -> Generator[str, None, None]: diff --git a/tests/system/large/functions/test_managed_function.py b/tests/system/large/functions/test_managed_function.py index 7c8c74e005..7001736bb2 100644 --- a/tests/system/large/functions/test_managed_function.py +++ b/tests/system/large/functions/test_managed_function.py @@ -21,6 +21,12 @@ import bigframes.pandas as bpd from tests.system.utils import cleanup_function_assets +# TODO(shobs): restore these tests after the managed udf cleanup issue is +# resolved in the test project +pytestmark = pytest.mark.skip( + reason="temporarily disable to debug managed udf cleanup in the test project" +) + bpd.options.experiments.udf = True From d553fa25fe85b3590269ed2ce08d5dff3bd22dfc Mon Sep 17 00:00:00 2001 From: Arwa Sharif <146148342+arwas11@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:25:17 -0500 Subject: [PATCH 18/39] docs: update `GeoSeries.difference()` and `bigframes.bigquery.st_difference()` docs (#1526) * docs: update GeoSeries.difference() and bigframes.bigquery.st_difference() docs * update variable names for readability --- bigframes/bigquery/_operations/geo.py | 26 ++++++------ .../bigframes_vendored/geopandas/geoseries.py | 41 ++++++++----------- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/bigframes/bigquery/_operations/geo.py b/bigframes/bigquery/_operations/geo.py index a41c33f67d..6501c84b6f 100644 --- a/bigframes/bigquery/_operations/geo.py +++ b/bigframes/bigquery/_operations/geo.py @@ -28,14 +28,14 @@ def st_area(series: bigframes.series.Series) -> bigframes.series.Series: """ Returns the area in square meters covered by the polygons in the input - GEOGRAPHY. + `GEOGRAPHY`. If geography_expression is a point or a line, returns zero. If geography_expression is a collection, returns the area of the polygons in the collection; if the collection doesn't contain polygons, returns zero. - ..note:: + .. note:: BigQuery's Geography functions, like `st_area`, interpret the geometry data type as a point set on the Earth's surface. A point set is a set of points, lines, and polygons on the WGS84 reference spheroid, with @@ -98,14 +98,14 @@ def st_difference( series: bigframes.series.Series, other: bigframes.series.Series ) -> bigframes.series.Series: """ - Returns a GEOGRAPHY that represents the point set difference of + Returns a `GEOGRAPHY` that represents the point set difference of `geography_1` and `geography_2`. Therefore, the result consists of the part of `geography_1` that doesn't intersect with `geography_2`. - If `geometry_1` is completely contained in `geometry_2`, then ST_DIFFERENCE - returns an empty GEOGRAPHY. + If `geometry_1` is completely contained in `geometry_2`, then `ST_DIFFERENCE` + returns an empty `GEOGRAPHY`. - ..note:: + .. note:: BigQuery's Geography functions, like `st_difference`, interpret the geometry data type as a point set on the Earth's surface. A point set is a set of points, lines, and polygons on the WGS84 reference spheroid, with @@ -119,7 +119,7 @@ def st_difference( >>> from shapely.geometry import Polygon, LineString, Point >>> bpd.options.display.progress_bar = None - We can check two GeoSeries against each other, row by row. + We can check two GeoSeries against each other, row by row: >>> s1 = bigframes.geopandas.GeoSeries( ... [ @@ -168,32 +168,32 @@ def st_difference( We can also check difference of single shapely geometries: - >>> sbq1 = bigframes.geopandas.GeoSeries( + >>> polygon_s1 = bigframes.geopandas.GeoSeries( ... [ ... Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]) ... ] ... ) - >>> sbq2 = bigframes.geopandas.GeoSeries( + >>> polygon_s2 = bigframes.geopandas.GeoSeries( ... [ ... Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]) ... ] ... ) - >>> sbq1 + >>> polygon_s1 0 POLYGON ((0 0, 10 0, 10 10, 0 0)) dtype: geometry - >>> sbq2 + >>> polygon_s2 0 POLYGON ((4 2, 6 2, 8 6, 4 2)) dtype: geometry - >>> bbq.st_difference(sbq1, sbq2) + >>> bbq.st_difference(polygon_s1, polygon_s2) 0 POLYGON ((0 0, 10 0, 10 10, 0 0), (8 6, 6 2, 4... dtype: geometry Additionally, we can check difference of a GeoSeries against a single shapely geometry: - >>> bbq.st_difference(s1, sbq2) + >>> bbq.st_difference(s1, polygon_s2) 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) 1 None 2 None diff --git a/third_party/bigframes_vendored/geopandas/geoseries.py b/third_party/bigframes_vendored/geopandas/geoseries.py index b00d4220ff..0d6b74671e 100644 --- a/third_party/bigframes_vendored/geopandas/geoseries.py +++ b/third_party/bigframes_vendored/geopandas/geoseries.py @@ -242,10 +242,10 @@ def to_wkt(self) -> bigframes.series.Series: def difference(self: GeoSeries, other: GeoSeries) -> GeoSeries: # type: ignore """ - Returns a GeoSeries of the points in each aligned geometry that are not - in other. + Returns a GeoSeries of the points in each aligned geometry that are not + in other. - The operation works on a 1-to-1 row-wise manner + The operation works on a 1-to-1 row-wise manner. **Examples:** @@ -254,7 +254,7 @@ def difference(self: GeoSeries, other: GeoSeries) -> GeoSeries: # type: ignore >>> from shapely.geometry import Polygon, LineString, Point >>> bpd.options.display.progress_bar = None - We can check two GeoSeries against each other, row by row. + We can check two GeoSeries against each other, row by row: >>> s1 = bigframes.geopandas.GeoSeries( ... [ @@ -303,32 +303,32 @@ def difference(self: GeoSeries, other: GeoSeries) -> GeoSeries: # type: ignore We can also check difference of single shapely geometries: - >>> sbq1 = bigframes.geopandas.GeoSeries( + >>> polygon_s1 = bigframes.geopandas.GeoSeries( ... [ ... Polygon([(0, 0), (10, 0), (10, 10), (0, 0)]) ... ] ... ) - >>> sbq2 = bigframes.geopandas.GeoSeries( + >>> polygon_s2 = bigframes.geopandas.GeoSeries( ... [ ... Polygon([(4, 2), (6, 2), (8, 6), (4, 2)]) ... ] ... ) - >>> sbq1 + >>> polygon_s1 0 POLYGON ((0 0, 10 0, 10 10, 0 0)) dtype: geometry - >>> sbq2 + >>> polygon_s2 0 POLYGON ((4 2, 6 2, 8 6, 4 2)) dtype: geometry - >>> sbq1.difference(sbq2) + >>> polygon_s1.difference(polygon_s2) 0 POLYGON ((0 0, 10 0, 10 10, 0 0), (8 6, 6 2, 4... dtype: geometry Additionally, we can check difference of a GeoSeries against a single shapely geometry: - >>> s1.difference(sbq2) + >>> s1.difference(polygon_s2) 0 POLYGON ((0 0, 2 2, 0 2, 0 0)) 1 None 2 None @@ -336,19 +336,14 @@ def difference(self: GeoSeries, other: GeoSeries) -> GeoSeries: # type: ignore 4 None dtype: geometry - Args: - other (GeoSeries or geometric object): - The GeoSeries (elementwise) or geometric object to find the - difference to. - - Returns: - bigframes.geopandas.GeoSeries: - A GeoSeries of the points in each aligned geometry that are not - in other. + Args: + other (bigframes.geopandas.GeoSeries or geometric object): + The GeoSeries (elementwise) or geometric object to find the + difference to. - Raises: - NotImplementedError: - GeoSeries.difference is not supported. Use - bigframes.bigquery.st_difference(series), instead. + Returns: + bigframes.geopandas.GeoSeries: + A GeoSeries of the points in each aligned geometry that are not + in other. """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) From 4b0cf572a921b66a3002ba269528223ddf2251a6 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 25 Mar 2025 10:54:58 -0700 Subject: [PATCH 19/39] chore: allow PRECEDING and FOLLOWING to appear on both side of BETWEEN when windowing (#1507) * chore: allow PRECEDING and FOLLOWING to appear on both side of BETWEEN when windowing * fix lint * Simplify the code by using the sign of the value for PRECEDING/FOLLOWING * fix lint * fix mypy * polish doc * remove float support for range rolling because Pandas does not support that --- bigframes/core/block_transforms.py | 6 +-- bigframes/core/compile/compiled.py | 47 ++++++++++++++------ bigframes/core/compile/polars/compiler.py | 33 +++++++------- bigframes/core/groupby/__init__.py | 8 ++-- bigframes/core/window_spec.py | 52 ++++++++++++++++------- bigframes/dataframe.py | 6 +-- bigframes/series.py | 6 +-- tests/unit/core/test_windowspec.py | 29 +++++++++++++ 8 files changed, 128 insertions(+), 59 deletions(-) create mode 100644 tests/unit/core/test_windowspec.py diff --git a/bigframes/core/block_transforms.py b/bigframes/core/block_transforms.py index 0e9525d5af..09ef17dff5 100644 --- a/bigframes/core/block_transforms.py +++ b/bigframes/core/block_transforms.py @@ -213,8 +213,8 @@ def _interpolate_column( if interpolate_method not in ["linear", "nearest", "ffill"]: raise ValueError("interpolate method not supported") window_ordering = (ordering.OrderingExpression(ex.deref(x_values)),) - backwards_window = windows.rows(following=0, ordering=window_ordering) - forwards_window = windows.rows(preceding=0, ordering=window_ordering) + backwards_window = windows.rows(end=0, ordering=window_ordering) + forwards_window = windows.rows(start=0, ordering=window_ordering) # Note, this method may block, notnull = block.apply_unary_op(column, ops.notnull_op) @@ -450,7 +450,7 @@ def rank( ) if method == "dense" else windows.rows( - following=0, ordering=window_ordering, grouping_keys=grouping_cols + end=0, ordering=window_ordering, grouping_keys=grouping_cols ), skip_reproject_unsafe=(col != columns[-1]), ) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index c3d4c10267..d2fd7f3ea2 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -21,7 +21,9 @@ import bigframes_vendored.ibis import bigframes_vendored.ibis.backends.bigquery.backend as ibis_bigquery import bigframes_vendored.ibis.common.deferred as ibis_deferred # type: ignore +from bigframes_vendored.ibis.expr import builders as ibis_expr_builders import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes +from bigframes_vendored.ibis.expr.operations import window as ibis_expr_window import bigframes_vendored.ibis.expr.operations as ibis_ops import bigframes_vendored.ibis.expr.types as ibis_types import pandas @@ -551,20 +553,9 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec): # Unbound grouping window. Suitable for aggregations but not for analytic function application. order_by = None - bounds = window_spec.bounds window = bigframes_vendored.ibis.window(order_by=order_by, group_by=group_by) - if bounds is not None: - if isinstance(bounds, RangeWindowBounds): - window = window.preceding_following( - bounds.preceding, bounds.following, how="range" - ) - if isinstance(bounds, RowsWindowBounds): - if bounds.preceding is not None or bounds.following is not None: - window = window.preceding_following( - bounds.preceding, bounds.following, how="rows" - ) - else: - raise ValueError(f"unrecognized window bounds {bounds}") + if window_spec.bounds is not None: + return _add_boundary(window_spec.bounds, window) return window @@ -681,3 +672,33 @@ def _as_groupable(value: ibis_types.Value): return scalar_op_compiler.to_json_string(value) else: return value + + +def _to_ibis_boundary( + boundary: Optional[int], +) -> Optional[ibis_expr_window.WindowBoundary]: + if boundary is None: + return None + return ibis_expr_window.WindowBoundary( + abs(boundary), preceding=boundary <= 0 # type:ignore + ) + + +def _add_boundary( + bounds: typing.Union[RowsWindowBounds, RangeWindowBounds], + ibis_window: ibis_expr_builders.LegacyWindowBuilder, +) -> ibis_expr_builders.LegacyWindowBuilder: + if isinstance(bounds, RangeWindowBounds): + return ibis_window.range( + start=_to_ibis_boundary(bounds.start), + end=_to_ibis_boundary(bounds.end), + ) + if isinstance(bounds, RowsWindowBounds): + if bounds.start is not None or bounds.end is not None: + return ibis_window.rows( + start=_to_ibis_boundary(bounds.start), + end=_to_ibis_boundary(bounds.end), + ) + return ibis_window + else: + raise ValueError(f"unrecognized window bounds {bounds}") diff --git a/bigframes/core/compile/polars/compiler.py b/bigframes/core/compile/polars/compiler.py index 6d5b11a5e8..6fac3c9b92 100644 --- a/bigframes/core/compile/polars/compiler.py +++ b/bigframes/core/compile/polars/compiler.py @@ -16,9 +16,10 @@ import dataclasses import functools import itertools -from typing import cast, Sequence, Tuple, TYPE_CHECKING +from typing import cast, Optional, Sequence, Tuple, TYPE_CHECKING, Union import bigframes.core +from bigframes.core import window_spec import bigframes.core.expression as ex import bigframes.core.guid as guid import bigframes.core.nodes as nodes @@ -366,23 +367,8 @@ def compile_window(self, node: nodes.WindowOpNode): indexed_df = df.with_row_index(index_col_name) if len(window.grouping_keys) == 0: # rolling-only window # https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.rolling.html - finite = ( - window.bounds.preceding is not None - and window.bounds.following is not None - ) - offset_n = ( - None - if window.bounds.preceding is None - else -window.bounds.preceding - ) - # collecting height is a massive kludge - period_n = ( - df.collect().height - if not finite - else cast(int, window.bounds.preceding) - + cast(int, window.bounds.following) - + 1 - ) + offset_n = window.bounds.start + period_n = _get_period(window.bounds) or df.collect().height results = indexed_df.rolling( index_column=index_col_name, period=f"{period_n}i", @@ -395,3 +381,14 @@ def compile_window(self, node: nodes.WindowOpNode): # polars is columnar, so this is efficient # TODO: why can't just add columns? return pl.concat([df, results], how="horizontal") + + +def _get_period( + bounds: Union[window_spec.RowsWindowBounds, window_spec.RangeWindowBounds] +) -> Optional[int]: + """Returns None if the boundary is infinite.""" + if bounds.start is None or bounds.end is None: + return None + + # collecting height is a massive kludge + return bounds.end - bounds.start + 1 diff --git a/bigframes/core/groupby/__init__.py b/bigframes/core/groupby/__init__.py index 126d2f4dd2..3134df0daf 100644 --- a/bigframes/core/groupby/__init__.py +++ b/bigframes/core/groupby/__init__.py @@ -309,8 +309,8 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: # To get n size window, need current row and n-1 preceding rows. window_spec = window_specs.rows( grouping_keys=tuple(self._by_col_ids), - preceding=window - 1, - following=0, + start=-(window - 1), + end=0, min_periods=min_periods or window, ) block = self._block.order_by( @@ -742,8 +742,8 @@ def rolling(self, window: int, min_periods=None) -> windows.Window: # To get n size window, need current row and n-1 preceding rows. window_spec = window_specs.rows( grouping_keys=tuple(self._by_col_ids), - preceding=window - 1, - following=0, + start=-(window - 1), + end=0, min_periods=min_periods or window, ) block = self._block.order_by( diff --git a/bigframes/core/window_spec.py b/bigframes/core/window_spec.py index b4a3d35471..a286234fc8 100644 --- a/bigframes/core/window_spec.py +++ b/bigframes/core/window_spec.py @@ -52,8 +52,8 @@ def unbound( ### Rows-based Windows def rows( grouping_keys: Tuple[str, ...] = (), - preceding: Optional[int] = None, - following: Optional[int] = None, + start: Optional[int] = None, + end: Optional[int] = None, min_periods: int = 0, ordering: Tuple[orderings.OrderingExpression, ...] = (), ) -> WindowSpec: @@ -63,10 +63,12 @@ def rows( Args: grouping_keys: Columns ids of grouping keys - preceding: - number of preceding rows to include. If None, include all preceding rows + start: + The window's starting boundary relative to the current row. For example, "-1" means one row prior + "1" means one row after, and "0" means the current row. If None, the window is unbounded from the start. following: - number of following rows to include. If None, include all following rows + The window's ending boundary relative to the current row. For example, "-1" means one row prior + "1" means one row after, and "0" means the current row. If None, the window is unbounded until the end. min_periods (int, default 0): Minimum number of input rows to generate output. ordering: @@ -74,7 +76,10 @@ def rows( Returns: WindowSpec """ - bounds = RowsWindowBounds(preceding=preceding, following=following) + bounds = RowsWindowBounds( + start=start, + end=end, + ) return WindowSpec( grouping_keys=tuple(map(ex.deref, grouping_keys)), bounds=bounds, @@ -97,7 +102,7 @@ def cumulative_rows( Returns: WindowSpec """ - bounds = RowsWindowBounds(following=0) + bounds = RowsWindowBounds(end=0) return WindowSpec( grouping_keys=tuple(map(ex.deref, grouping_keys)), bounds=bounds, @@ -119,7 +124,7 @@ def inverse_cumulative_rows( Returns: WindowSpec """ - bounds = RowsWindowBounds(preceding=0) + bounds = RowsWindowBounds(start=0) return WindowSpec( grouping_keys=tuple(map(ex.deref, grouping_keys)), bounds=bounds, @@ -132,18 +137,35 @@ def inverse_cumulative_rows( @dataclass(frozen=True) class RowsWindowBounds: - preceding: Optional[int] = None - following: Optional[int] = None - + start: Optional[int] = None + end: Optional[int] = None -# TODO: Expand to datetime offsets -OffsetType = Union[float, int] + def __post_init__(self): + if self.start is None: + return + if self.end is None: + return + if self.start > self.end: + raise ValueError( + f"Invalid window: start({self.start}) is greater than end({self.end})" + ) @dataclass(frozen=True) class RangeWindowBounds: - preceding: Optional[OffsetType] = None - following: Optional[OffsetType] = None + # TODO(b/388916840) Support range rolling on timeseries with timedeltas. + start: Optional[int] = None + end: Optional[int] = None + + def __post_init__(self): + if self.start is None: + return + if self.end is None: + return + if self.start > self.end: + raise ValueError( + f"Invalid window: start({self.start}) is greater than end({self.end})" + ) @dataclass(frozen=True) diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 1d3a45e879..9fd6ff5c7b 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -2428,12 +2428,12 @@ def replace( @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = windows.rows(preceding=limit, following=0) + window = windows.rows(start=None if limit is None else -limit, end=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> DataFrame: - window = windows.rows(preceding=0, following=limit) + window = windows.rows(start=0, end=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) def isin(self, values) -> DataFrame: @@ -3310,7 +3310,7 @@ def _perform_join_by_index( def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window: # To get n size window, need current row and n-1 preceding rows. window_def = windows.rows( - preceding=window - 1, following=0, min_periods=min_periods or window + start=-(window - 1), end=0, min_periods=min_periods or window ) return bigframes.core.window.Window( self._block, window_def, self._block.value_columns diff --git a/bigframes/series.py b/bigframes/series.py index 5f49daa07d..a33a3fca5c 100644 --- a/bigframes/series.py +++ b/bigframes/series.py @@ -544,7 +544,7 @@ def cumsum(self) -> Series: @validations.requires_ordering() def ffill(self, *, limit: typing.Optional[int] = None) -> Series: - window = windows.rows(preceding=limit, following=0) + window = windows.rows(start=None if limit is None else -limit, end=0) return self._apply_window_op(agg_ops.LastNonNullOp(), window) pad = ffill @@ -552,7 +552,7 @@ def ffill(self, *, limit: typing.Optional[int] = None) -> Series: @validations.requires_ordering() def bfill(self, *, limit: typing.Optional[int] = None) -> Series: - window = windows.rows(preceding=0, following=limit) + window = windows.rows(start=0, end=limit) return self._apply_window_op(agg_ops.FirstNonNullOp(), window) @validations.requires_ordering() @@ -1441,7 +1441,7 @@ def sort_index(self, *, axis=0, ascending=True, na_position="last") -> Series: def rolling(self, window: int, min_periods=None) -> bigframes.core.window.Window: # To get n size window, need current row and n-1 preceding rows. window_spec = windows.rows( - preceding=window - 1, following=0, min_periods=min_periods or window + start=-(window - 1), end=0, min_periods=min_periods or window ) return bigframes.core.window.Window( self._block, window_spec, self._block.value_columns, is_series=True diff --git a/tests/unit/core/test_windowspec.py b/tests/unit/core/test_windowspec.py new file mode 100644 index 0000000000..a630c87dda --- /dev/null +++ b/tests/unit/core/test_windowspec.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from bigframes.core import window_spec + + +@pytest.mark.parametrize(("start", "end"), [(-1, -2), (1, -2), (2, 1)]) +def test_invalid_rows_window_boundary_raise_error(start, end): + with pytest.raises(ValueError): + window_spec.RowsWindowBounds(start, end) + + +@pytest.mark.parametrize(("start", "end"), [(-1, -2), (1, -2), (2, 1)]) +def test_invalid_range_window_boundary_raise_error(start, end): + with pytest.raises(ValueError): + window_spec.RangeWindowBounds(start, end) From 803c6dd2b35d69723ebeccce9feb2c56a12c680f Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Tue, 25 Mar 2025 11:37:31 -0700 Subject: [PATCH 20/39] chore: include source columns in semantics.top_k (#1531) Co-authored-by: Shenyang Cai --- bigframes/operations/semantics.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index f4b9d85103..3b22f19539 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -807,13 +807,17 @@ def top_k( >>> import bigframes.ml.llm as llm >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") - >>> df = bpd.DataFrame({"Animals": ["Dog", "Bird", "Cat", "Horse"]}) + >>> df = bpd.DataFrame( + ... { + ... "Animals": ["Dog", "Bird", "Cat", "Horse"], + ... "Sounds": ["Woof", "Chirp", "Meow", "Neigh"], + ... }) >>> df.semantics.top_k("{Animals} are more popular as pets", model=model, k=2) - Animals - 0 Dog - 2 Cat + Animals Sounds + 0 Dog Woof + 2 Cat Meow - [2 rows x 1 columns] + [2 rows x 2 columns] Args: instruction (str): @@ -911,14 +915,8 @@ def top_k( ) num_selected += num_new_selected - df = ( - df[df[status_column] > 0] - .drop(["index", status_column], axis=1) - .rename(columns={"old_index": "index"}) - .set_index("index") - ) - df.index.name = None - return df + result_df: bigframes.dataframe.DataFrame = self._df.copy() + return result_df[df.set_index("old_index")[status_column] > 0.0] @staticmethod def _topk_partition( From ac59173f5cc10cf31d7545147008aff98603e499 Mon Sep 17 00:00:00 2001 From: Shenyang Cai Date: Tue, 25 Mar 2025 12:43:50 -0700 Subject: [PATCH 21/39] refactor: introduce ai operator namespace and deprecated semantics (#1511) * refactor: introduce ai operator namespace and deprecated semantics * duplicate semantic op options too * improve test coverage * relax test condition * clean up semantics and add public docs * addressing comments * use FutureWarning for deprecation * copy over recent top_k changes --- bigframes/_config/compute_options.py | 23 +- bigframes/_config/experiment_options.py | 18 +- bigframes/dataframe.py | 10 + bigframes/operations/ai.py | 896 ++++++++++++++++ bigframes/operations/semantics.py | 8 +- docs/reference/bigframes.pandas/frame.rst | 8 + docs/templates/toc.yml | 2 + tests/system/large/operations/test_ai.py | 956 ++++++++++++++++++ tests/system/small/operations/test_ai.py | 141 +++ .../system/small/operations/test_semantics.py | 19 + tests/unit/_config/test_experiment_options.py | 17 +- tests/unit/test_dataframe.py | 11 + 12 files changed, 2098 insertions(+), 11 deletions(-) create mode 100644 bigframes/operations/ai.py create mode 100644 tests/system/large/operations/test_ai.py create mode 100644 tests/system/small/operations/test_ai.py diff --git a/bigframes/_config/compute_options.py b/bigframes/_config/compute_options.py index 21b41eb185..eb287f6065 100644 --- a/bigframes/_config/compute_options.py +++ b/bigframes/_config/compute_options.py @@ -60,19 +60,31 @@ class ComputeOptions: bytes billed beyond this limit will fail (without incurring a charge). If unspecified, this will be set to your project default. See `maximum_bytes_billed`: https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.job.QueryJobConfig#google_cloud_bigquery_job_QueryJobConfig_maximum_bytes_billed. + enable_multi_query_execution (bool, Options): If enabled, large queries may be factored into multiple smaller queries in order to avoid generating queries that are too complex for the query engine to handle. However this comes at the cost of increase cost and latency. + extra_query_labels (Dict[str, Any], Options): Stores additional custom labels for query configuration. - semmantic_ops_confirmation_threshold (int, optional): - Guards against unexepcted processing of large amount of rows by semantic operators. + + semantic_ops_confirmation_threshold (int, optional): + .. deprecated:: 1.42.0 + Semantic operators are deprecated. Please use AI operators instead + + semantic_ops_threshold_autofail (bool): + .. deprecated:: 1.42.0 + Semantic operators are deprecated. Please use AI operators instead + + ai_ops_confirmation_threshold (int, optional): + Guards against unexpected processing of large amount of rows by semantic operators. If the number of rows exceeds the threshold, the user will be asked to confirm their operations to resume. The default value is 0. Set the value to None to turn off the guard. - semantic_ops_threshold_autofail (bool): - Guards against unexepcted processing of large amount of rows by semantic operators. + + ai_ops_threshold_autofail (bool): + Guards against unexpected processing of large amount of rows by semantic operators. When set to True, the operation automatically fails without asking for user inputs. """ @@ -84,6 +96,9 @@ class ComputeOptions: semantic_ops_confirmation_threshold: Optional[int] = 0 semantic_ops_threshold_autofail = False + ai_ops_confirmation_threshold: Optional[int] = 0 + ai_ops_threshold_autofail = False + def assign_extra_query_labels(self, **kwargs: Any) -> None: """ Assigns additional custom labels for query configuration. The method updates the diff --git a/bigframes/_config/experiment_options.py b/bigframes/_config/experiment_options.py index 3d52976004..abe465de50 100644 --- a/bigframes/_config/experiment_options.py +++ b/bigframes/_config/experiment_options.py @@ -24,6 +24,7 @@ class ExperimentOptions: def __init__(self): self._semantic_operators: bool = False + self._ai_operators: bool = False self._blob: bool = False self._udf: bool = False @@ -35,11 +36,24 @@ def semantic_operators(self) -> bool: def semantic_operators(self, value: bool): if value is True: msg = bfe.format_message( - "Semantic operators are still under experiments, and are subject " + "Semantic operators are deprecated, and will be removed in the future" + ) + warnings.warn(msg, category=FutureWarning) + self._semantic_operators = value + + @property + def ai_operators(self) -> bool: + return self._ai_operators + + @ai_operators.setter + def ai_operators(self, value: bool): + if value is True: + msg = bfe.format_message( + "AI operators are still under experiments, and are subject " "to change in the future." ) warnings.warn(msg, category=bfe.PreviewWarning) - self._semantic_operators = value + self._ai_operators = value @property def blob(self) -> bool: diff --git a/bigframes/dataframe.py b/bigframes/dataframe.py index 9fd6ff5c7b..b0648d65ba 100644 --- a/bigframes/dataframe.py +++ b/bigframes/dataframe.py @@ -74,6 +74,7 @@ import bigframes.operations as ops import bigframes.operations.aggregations import bigframes.operations.aggregations as agg_ops +import bigframes.operations.ai import bigframes.operations.plotting as plotting import bigframes.operations.semantics import bigframes.operations.structs @@ -4574,4 +4575,13 @@ def _throw_if_null_index(self, opname: str): @property def semantics(self): + msg = bfe.format_message( + "The 'semantics' property will be removed. Please use 'ai' instead." + ) + warnings.warn(msg, category=FutureWarning) return bigframes.operations.semantics.Semantics(self) + + @property + def ai(self): + """Returns the accessor for AI operators.""" + return bigframes.operations.ai.AIAccessor(self) diff --git a/bigframes/operations/ai.py b/bigframes/operations/ai.py new file mode 100644 index 0000000000..0ff92187cf --- /dev/null +++ b/bigframes/operations/ai.py @@ -0,0 +1,896 @@ +# 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 re +import typing +from typing import List, Optional +import warnings + +import numpy as np + +from bigframes import dtypes, exceptions +from bigframes.core import guid, log_adapter + + +@log_adapter.class_logger +class AIAccessor: + def __init__(self, df) -> None: + import bigframes # Import in the function body to avoid circular imports. + import bigframes.dataframe + + if not bigframes.options.experiments.ai_operators: + raise NotImplementedError() + + self._df: bigframes.dataframe.DataFrame = df + + def filter(self, instruction: str, model, ground_with_google_search: bool = False): + """ + Filters the DataFrame with the semantics of the user instruction. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") + + >>> df = bpd.DataFrame({"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}) + >>> df.ai.filter("{city} is the capital of {country}", model) + country city + 1 Germany Berlin + + [1 rows x 2 columns] + + Args: + instruction (str): + An instruction on how to filter the data. This value must contain + column references by name, which should be wrapped in a pair of braces. + For example, if you have a column "food", you can refer to this column + in the instructions like: + "The {food} is healthy." + + model (bigframes.ml.llm.GeminiTextGenerator): + A GeminiTextGenerator provided by Bigframes ML package. + + ground_with_google_search (bool, default False): + Enables Grounding with Google Search for the GeminiTextGenerator model. + When set to True, the model incorporates relevant information from Google + Search results into its responses, enhancing their accuracy and factualness. + Note: Using this feature may impact billing costs. Refer to the pricing + page for details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models + The default is `False`. + + Returns: + bigframes.pandas.DataFrame: DataFrame filtered by the instruction. + + Raises: + NotImplementedError: when the AI operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + import bigframes.dataframe + import bigframes.series + + self._validate_model(model) + columns = self._parse_columns(instruction) + for column in columns: + if column not in self._df.columns: + raise ValueError(f"Column {column} not found.") + + if ground_with_google_search: + msg = exceptions.format_message( + "Enables Grounding with Google Search may impact billing cost. See pricing " + "details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models" + ) + warnings.warn(msg, category=UserWarning) + + self._confirm_operation(len(self._df)) + + df: bigframes.dataframe.DataFrame = self._df[columns].copy() + has_blob_column = False + for column in columns: + if df[column].dtype == dtypes.OBJ_REF_DTYPE: + # Don't cast blob columns to string + has_blob_column = True + continue + + if df[column].dtype != dtypes.STRING_DTYPE: + df[column] = df[column].astype(dtypes.STRING_DTYPE) + + user_instruction = self._format_instruction(instruction, columns) + output_instruction = "Based on the provided context, reply to the following claim by only True or False:" + + if has_blob_column: + results = typing.cast( + bigframes.dataframe.DataFrame, + model.predict( + df, + prompt=self._make_multimodel_prompt( + df, columns, user_instruction, output_instruction + ), + temperature=0.0, + ground_with_google_search=ground_with_google_search, + ), + ) + else: + results = typing.cast( + bigframes.dataframe.DataFrame, + model.predict( + self._make_text_prompt( + df, columns, user_instruction, output_instruction + ), + temperature=0.0, + ground_with_google_search=ground_with_google_search, + ), + ) + + return self._df[ + results["ml_generate_text_llm_result"].str.lower().str.contains("true") + ] + + def map( + self, + instruction: str, + output_column: str, + model, + ground_with_google_search: bool = False, + ): + """ + Maps the DataFrame with the semantics of the user instruction. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") + + >>> df = bpd.DataFrame({"ingredient_1": ["Burger Bun", "Soy Bean"], "ingredient_2": ["Beef Patty", "Bittern"]}) + >>> df.ai.map("What is the food made from {ingredient_1} and {ingredient_2}? One word only.", output_column="food", model=model) + ingredient_1 ingredient_2 food + 0 Burger Bun Beef Patty Burger + + 1 Soy Bean Bittern Tofu + + + [2 rows x 3 columns] + + Args: + instruction (str): + An instruction on how to map the data. This value must contain + column references by name, which should be wrapped in a pair of braces. + For example, if you have a column "food", you can refer to this column + in the instructions like: + "Get the ingredients of {food}." + + output_column (str): + The column name of the mapping result. + + model (bigframes.ml.llm.GeminiTextGenerator): + A GeminiTextGenerator provided by Bigframes ML package. + + ground_with_google_search (bool, default False): + Enables Grounding with Google Search for the GeminiTextGenerator model. + When set to True, the model incorporates relevant information from Google + Search results into its responses, enhancing their accuracy and factualness. + Note: Using this feature may impact billing costs. Refer to the pricing + page for details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models + The default is `False`. + + Returns: + bigframes.pandas.DataFrame: DataFrame with attached mapping results. + + Raises: + NotImplementedError: when the AI operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + import bigframes.dataframe + import bigframes.series + + self._validate_model(model) + columns = self._parse_columns(instruction) + for column in columns: + if column not in self._df.columns: + raise ValueError(f"Column {column} not found.") + + if ground_with_google_search: + msg = exceptions.format_message( + "Enables Grounding with Google Search may impact billing cost. See pricing " + "details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models" + ) + warnings.warn(msg, category=UserWarning) + + self._confirm_operation(len(self._df)) + + df: bigframes.dataframe.DataFrame = self._df[columns].copy() + has_blob_column = False + for column in columns: + if df[column].dtype == dtypes.OBJ_REF_DTYPE: + # Don't cast blob columns to string + has_blob_column = True + continue + + if df[column].dtype != dtypes.STRING_DTYPE: + df[column] = df[column].astype(dtypes.STRING_DTYPE) + + user_instruction = self._format_instruction(instruction, columns) + output_instruction = ( + "Based on the provided contenxt, answer the following instruction:" + ) + + if has_blob_column: + results = typing.cast( + bigframes.series.Series, + model.predict( + df, + prompt=self._make_multimodel_prompt( + df, columns, user_instruction, output_instruction + ), + temperature=0.0, + ground_with_google_search=ground_with_google_search, + )["ml_generate_text_llm_result"], + ) + else: + results = typing.cast( + bigframes.series.Series, + model.predict( + self._make_text_prompt( + df, columns, user_instruction, output_instruction + ), + temperature=0.0, + ground_with_google_search=ground_with_google_search, + )["ml_generate_text_llm_result"], + ) + + from bigframes.core.reshape.api import concat + + return concat([self._df, results.rename(output_column)], axis=1) + + def join( + self, + other, + instruction: str, + model, + ground_with_google_search: bool = False, + ): + """ + Joines two dataframes by applying the instruction over each pair of rows from + the left and right table. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") + + >>> cities = bpd.DataFrame({'city': ['Seattle', 'Ottawa', 'Berlin', 'Shanghai', 'New Delhi']}) + >>> continents = bpd.DataFrame({'continent': ['North America', 'Africa', 'Asia']}) + + >>> cities.ai.join(continents, "{city} is in {continent}", model) + city continent + 0 Seattle North America + 1 Ottawa North America + 2 Shanghai Asia + 3 New Delhi Asia + + [4 rows x 2 columns] + + Args: + other (bigframes.pandas.DataFrame): + The other dataframe. + + instruction (str): + An instruction on how left and right rows can be joined. This value must contain + column references by name. which should be wrapped in a pair of braces. + For example: "The {city} belongs to the {country}". + For column names that are shared between two dataframes, you need to add "left." + and "right." prefix for differentiation. This is especially important when you do + self joins. For example: "The {left.employee_name} reports to {right.employee_name}" + For unique column names, this prefix is optional. + + model (bigframes.ml.llm.GeminiTextGenerator): + A GeminiTextGenerator provided by Bigframes ML package. + + max_rows (int, default 1000): + The maximum number of rows allowed to be sent to the model per call. If the result is too large, the method + call will end early with an error. + + ground_with_google_search (bool, default False): + Enables Grounding with Google Search for the GeminiTextGenerator model. + When set to True, the model incorporates relevant information from Google + Search results into its responses, enhancing their accuracy and factualness. + Note: Using this feature may impact billing costs. Refer to the pricing + page for details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models + The default is `False`. + + Returns: + bigframes.pandas.DataFrame: The joined dataframe. + + Raises: + ValueError if the amount of data that will be sent for LLM processing is larger than max_rows. + """ + self._validate_model(model) + columns = self._parse_columns(instruction) + + if ground_with_google_search: + msg = exceptions.format_message( + "Enables Grounding with Google Search may impact billing cost. See pricing " + "details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models" + ) + warnings.warn(msg, category=UserWarning) + + work_estimate = len(self._df) * len(other) + self._confirm_operation(work_estimate) + + left_columns = [] + right_columns = [] + + for col in columns: + if col in self._df.columns and col in other.columns: + raise ValueError(f"Ambiguous column reference: {col}") + + elif col in self._df.columns: + left_columns.append(col) + + elif col in other.columns: + right_columns.append(col) + + elif col.startswith("left."): + original_col_name = col[len("left.") :] + if ( + original_col_name in self._df.columns + and original_col_name in other.columns + ): + left_columns.append(col) + elif original_col_name in self._df.columns: + left_columns.append(col) + instruction = instruction.replace(col, original_col_name) + else: + raise ValueError(f"Column {col} not found") + + elif col.startswith("right."): + original_col_name = col[len("right.") :] + if ( + original_col_name in self._df.columns + and original_col_name in other.columns + ): + right_columns.append(col) + elif original_col_name in other.columns: + right_columns.append(col) + instruction = instruction.replace(col, original_col_name) + else: + raise ValueError(f"Column {col} not found") + + else: + raise ValueError(f"Column {col} not found") + + if not left_columns: + raise ValueError("No left column references.") + + if not right_columns: + raise ValueError("No right column references.") + + # Update column references to be compatible with internal naming scheme. + # That is, "left.col" -> "col_left" and "right.col" -> "col_right" + instruction = re.sub(r"(?>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + + >>> import bigframes + >>> bigframes.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.TextEmbeddingGenerator(model_name="text-embedding-005") + + >>> df = bpd.DataFrame({"creatures": ["salmon", "sea urchin", "frog", "chimpanzee"]}) + >>> df.ai.search("creatures", "monkey", top_k=1, model=model, score_column='distance') + creatures distance + 3 chimpanzee 0.635844 + + [1 rows x 2 columns] + + Args: + search_column: + The name of the column to search from. + query (str): + The search query. + top_k (int): + The number of nearest neighbors to return. + model (TextEmbeddingGenerator): + A TextEmbeddingGenerator provided by Bigframes ML package. + score_column (Optional[str], default None): + The name of the the additional column containning the similarity scores. If None, + this column won't be attached to the result. + + Returns: + DataFrame: the DataFrame with the search result. + + Raises: + ValueError: when the search_column is not found from the the data frame. + TypeError: when the provided model is not TextEmbeddingGenerator. + """ + + if search_column not in self._df.columns: + raise ValueError(f"Column `{search_column}` not found") + + self._confirm_operation(len(self._df)) + + import bigframes.ml.llm as llm + + if not isinstance(model, llm.TextEmbeddingGenerator): + raise TypeError(f"Expect a text embedding model, but got: {type(model)}") + + if top_k < 1: + raise ValueError("top_k must be an integer greater than or equal to 1.") + + embedded_df = model.predict(self._df[search_column]) + embedded_table = embedded_df.reset_index().to_gbq() + + import bigframes.pandas as bpd + + embedding_result_column = "ml_generate_embedding_result" + query_df = model.predict(bpd.DataFrame({"query_id": [query]})).rename( + columns={"content": "query_id", embedding_result_column: "embedding"} + ) + + import bigframes.bigquery as bbq + + search_result = ( + bbq.vector_search( + base_table=embedded_table, + column_to_search=embedding_result_column, + query=query_df, + top_k=top_k, + ) + .rename(columns={"content": search_column}) + .set_index("index") + ) + + search_result.index.name = self._df.index.name + + if score_column is not None: + search_result = search_result.rename(columns={"distance": score_column})[ + [search_column, score_column] + ] + else: + search_result = search_result[[search_column]] + + import bigframes.dataframe + + return typing.cast(bigframes.dataframe.DataFrame, search_result) + + def top_k( + self, + instruction: str, + model, + k: int = 10, + ground_with_google_search: bool = False, + ): + """ + Ranks each tuple and returns the k best according to the instruction. + + This method employs a quick select algorithm to efficiently compare the pivot + with all other items. By leveraging an LLM (Large Language Model), it then + identifies the top 'k' best answers from these comparisons. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.GeminiTextGenerator(model_name="gemini-1.5-flash-001") + + >>> df = bpd.DataFrame( + ... { + ... "Animals": ["Dog", "Bird", "Cat", "Horse"], + ... "Sounds": ["Woof", "Chirp", "Meow", "Neigh"], + ... }) + >>> df.ai.top_k("{Animals} are more popular as pets", model=model, k=2) + Animals Sounds + 0 Dog Woof + 2 Cat Meow + + [2 rows x 2 columns] + + Args: + instruction (str): + An instruction on how to map the data. This value must contain + column references by name enclosed in braces. + For example, to reference a column named "Animals", use "{Animals}" in the + instruction, like: "{Animals} are more popular as pets" + + model (bigframes.ml.llm.GeminiTextGenerator): + A GeminiTextGenerator provided by the Bigframes ML package. + + k (int, default 10): + The number of rows to return. + + ground_with_google_search (bool, default False): + Enables Grounding with Google Search for the GeminiTextGenerator model. + When set to True, the model incorporates relevant information from Google + Search results into its responses, enhancing their accuracy and factualness. + Note: Using this feature may impact billing costs. Refer to the pricing + page for details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models + The default is `False`. + + Returns: + bigframes.dataframe.DataFrame: A new DataFrame with the top k rows. + + Raises: + NotImplementedError: when the AI operator experiment is off. + ValueError: when the instruction refers to a non-existing column, or when no + columns are referred to. + """ + import bigframes.dataframe + import bigframes.series + + self._validate_model(model) + columns = self._parse_columns(instruction) + for column in columns: + if column not in self._df.columns: + raise ValueError(f"Column {column} not found.") + if len(columns) > 1: + raise NotImplementedError("AI top K are limited to a single column.") + + if ground_with_google_search: + msg = exceptions.format_message( + "Enables Grounding with Google Search may impact billing cost. See pricing " + "details: https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models" + ) + warnings.warn(msg, category=UserWarning) + + work_estimate = int(len(self._df) * (len(self._df) - 1) / 2) + self._confirm_operation(work_estimate) + + df: bigframes.dataframe.DataFrame = self._df[columns].copy() + column = columns[0] + if df[column].dtype != dtypes.STRING_DTYPE: + df[column] = df[column].astype(dtypes.STRING_DTYPE) + + # `index` is reserved for the `reset_index` below. + if column == "index": + raise ValueError( + "Column name 'index' is reserved. Please choose a different name." + ) + + if k < 1: + raise ValueError("k must be an integer greater than or equal to 1.") + + user_instruction = self._format_instruction(instruction, columns) + + n = df.shape[0] + if k >= n: + return df + + # Create a unique index and duplicate it as the "index" column. This workaround + # is needed for the select search algorithm due to unimplemented bigFrame methods. + df = df.reset_index().rename(columns={"index": "old_index"}).reset_index() + + # Initialize a status column to track the selection status of each item. + # - None: Unknown/not yet processed + # - 1.0: Selected as part of the top-k items + # - -1.0: Excluded from the top-k items + status_column = guid.generate_guid("status") + df[status_column] = bigframes.series.Series( + None, dtype=dtypes.FLOAT_DTYPE, session=df._session + ) + + num_selected = 0 + while num_selected < k: + df, num_new_selected = self._topk_partition( + df, + column, + status_column, + user_instruction, + model, + k - num_selected, + ground_with_google_search, + ) + num_selected += num_new_selected + + result_df: bigframes.dataframe.DataFrame = self._df.copy() + return result_df[df.set_index("old_index")[status_column] > 0.0] + + @staticmethod + def _topk_partition( + df, + column: str, + status_column: str, + user_instruction: str, + model, + k: int, + ground_with_google_search: bool, + ): + output_instruction = ( + "Given a question and two documents, choose the document that best answers " + "the question. Respond with 'Document 1' or 'Document 2'. You must choose " + "one, even if neither is ideal. " + ) + + # Random pivot selection for improved average quickselect performance. + pending_df = df[df[status_column].isna()] + pivot_iloc = np.random.randint(0, pending_df.shape[0]) + pivot_index = pending_df.iloc[pivot_iloc]["index"] + pivot_df = pending_df[pending_df["index"] == pivot_index] + + # Build a prompt to compare the pivot item's relevance to other pending items. + prompt_s = pending_df[pending_df["index"] != pivot_index][column] + prompt_s = ( + f"{output_instruction}\n\nQuestion: {user_instruction}\n" + + f"\nDocument 1: {column} " + + pivot_df.iloc[0][column] + + f"\nDocument 2: {column} " + + prompt_s # type:ignore + ) + + import bigframes.dataframe + + predict_df = typing.cast( + bigframes.dataframe.DataFrame, + model.predict( + prompt_s, + temperature=0.0, + ground_with_google_search=ground_with_google_search, + ), + ) + + marks = predict_df["ml_generate_text_llm_result"].str.contains("2") + more_relavant: bigframes.dataframe.DataFrame = df[marks] + less_relavent: bigframes.dataframe.DataFrame = df[~marks] + + num_more_relavant = more_relavant.shape[0] + if k < num_more_relavant: + less_relavent[status_column] = -1.0 + pivot_df[status_column] = -1.0 + df = df.combine_first(less_relavent).combine_first(pivot_df) + return df, 0 + else: # k >= num_more_relavant + more_relavant[status_column] = 1.0 + df = df.combine_first(more_relavant) + if k >= num_more_relavant + 1: + pivot_df[status_column] = 1.0 + df = df.combine_first(pivot_df) + return df, num_more_relavant + 1 + else: + return df, num_more_relavant + + def sim_join( + self, + other, + left_on: str, + right_on: str, + model, + top_k: int = 3, + score_column: Optional[str] = None, + max_rows: int = 1000, + ): + """ + Joins two dataframes based on the similarity of the specified columns. + + This method uses BigQuery's VECTOR_SEARCH function to match rows on the left side with the rows that have + nearest embedding vectors on the right. In the worst case scenario, the complexity is around O(M * N * log K). + Therefore, this is a potentially expensive operation. + + ** Examples: ** + + >>> import bigframes.pandas as bpd + >>> bpd.options.display.progress_bar = None + >>> bpd.options.experiments.ai_operators = True + >>> bpd.options.compute.ai_ops_confirmation_threshold = 25 + + >>> import bigframes.ml.llm as llm + >>> model = llm.TextEmbeddingGenerator(model_name="text-embedding-005") + + >>> df1 = bpd.DataFrame({'animal': ['monkey', 'spider']}) + >>> df2 = bpd.DataFrame({'animal': ['scorpion', 'baboon']}) + + >>> df1.ai.sim_join(df2, left_on='animal', right_on='animal', model=model, top_k=1) + animal animal_1 + 0 monkey baboon + 1 spider scorpion + + [2 rows x 2 columns] + + Args: + other (DataFrame): + The other data frame to join with. + left_on (str): + The name of the column on left side for the join. + right_on (str): + The name of the column on the right side for the join. + top_k (int, default 3): + The number of nearest neighbors to return. + model (TextEmbeddingGenerator): + A TextEmbeddingGenerator provided by Bigframes ML package. + score_column (Optional[str], default None): + The name of the the additional column containning the similarity scores. If None, + this column won't be attached to the result. + max_rows: + The maximum number of rows allowed to be processed per call. If the result is too large, the method + call will end early with an error. + + Returns: + DataFrame: the data frame with the join result. + + Raises: + ValueError: when the amount of data to be processed exceeds the specified max_rows. + """ + + if left_on not in self._df.columns: + raise ValueError(f"Left column {left_on} not found") + if right_on not in self._df.columns: + raise ValueError(f"Right column {right_on} not found") + + import bigframes.ml.llm as llm + + if not isinstance(model, llm.TextEmbeddingGenerator): + raise TypeError(f"Expect a text embedding model, but got: {type(model)}") + + joined_table_rows = len(self._df) * len(other) + if joined_table_rows > max_rows: + raise ValueError( + f"Number of rows that need processing is {joined_table_rows}, which exceeds row limit {max_rows}." + ) + + if top_k < 1: + raise ValueError("top_k must be an integer greater than or equal to 1.") + + work_estimate = len(self._df) * len(other) + self._confirm_operation(work_estimate) + + base_table_embedding_column = guid.generate_guid() + base_table = self._attach_embedding( + other, right_on, base_table_embedding_column, model + ).to_gbq() + query_table = self._attach_embedding(self._df, left_on, "embedding", model) + + import bigframes.bigquery as bbq + + join_result = bbq.vector_search( + base_table=base_table, + column_to_search=base_table_embedding_column, + query=query_table, + top_k=top_k, + ) + + join_result = join_result.drop( + ["embedding", base_table_embedding_column], axis=1 + ) + + if score_column is not None: + join_result = join_result.rename(columns={"distance": score_column}) + else: + del join_result["distance"] + + return join_result + + @staticmethod + def _attach_embedding(dataframe, source_column: str, embedding_column: str, model): + result_df = dataframe.copy() + embeddings = model.predict(dataframe[source_column])[ + "ml_generate_embedding_result" + ] + result_df[embedding_column] = embeddings + return result_df + + @staticmethod + def _make_multimodel_prompt( + prompt_df, columns, user_instruction: str, output_instruction: str + ): + prompt = [f"{output_instruction}\n{user_instruction}\nContext: "] + for col in columns: + prompt.extend([f"{col} is ", prompt_df[col]]) + + return prompt + + @staticmethod + def _make_text_prompt( + prompt_df, columns, user_instruction: str, output_instruction: str + ): + prompt_df["prompt"] = f"{output_instruction}\n{user_instruction}\nContext: " + + # Combine context from multiple columns. + for col in columns: + prompt_df["prompt"] += f"{col} is `" + prompt_df[col] + "`\n" + + return prompt_df["prompt"] + + @staticmethod + def _parse_columns(instruction: str) -> List[str]: + """Extracts column names enclosed in curly braces from the user instruction. + For example, _parse_columns("{city} is in {continent}") == ["city", "continent"] + """ + columns = re.findall(r"(? str: + """Extracts column names enclosed in curly braces from the user instruction. + For example, `_format_instruction(["city", "continent"], "{city} is in {continent}") + == "city is in continent"` + """ + return instruction.format(**{col: col for col in columns}) + + @staticmethod + def _validate_model(model): + from bigframes.ml.llm import GeminiTextGenerator + + if not isinstance(model, GeminiTextGenerator): + raise TypeError("Model is not GeminiText Generator") + + @staticmethod + def _confirm_operation(row_count: int): + """Raises OperationAbortedError when the confirmation fails""" + import bigframes # Import in the function body to avoid circular imports. + + threshold = bigframes.options.compute.ai_ops_confirmation_threshold + + if threshold is None or row_count <= threshold: + return + + if bigframes.options.compute.ai_ops_threshold_autofail: + raise exceptions.OperationAbortedError( + f"Operation was cancelled because your work estimate is {row_count} rows, which exceeds the threshold {threshold} rows." + ) + + # Separate the prompt out. In IDE such VS Code, leaving prompt in the + # input function makes it less visible to the end user. + print(f"This operation will process about {row_count} rows.") + print( + "You can raise the confirmation threshold by setting `bigframes.options.compute.ai_ops_confirmation_threshold` to a higher value. To completely turn off the confirmation check, set the threshold to `None`." + ) + print("Proceed? [Y/n]") + reply = input().casefold() + if reply not in {"y", "yes", ""}: + raise exceptions.OperationAbortedError("Operation was cancelled.") diff --git a/bigframes/operations/semantics.py b/bigframes/operations/semantics.py index 3b22f19539..d1089f993e 100644 --- a/bigframes/operations/semantics.py +++ b/bigframes/operations/semantics.py @@ -857,9 +857,7 @@ def top_k( if column not in self._df.columns: raise ValueError(f"Column {column} not found.") if len(columns) > 1: - raise NotImplementedError( - "Semantic aggregations are limited to a single column." - ) + raise NotImplementedError("Semantic top K are limited to a single column.") if ground_with_google_search: msg = exceptions.format_message( @@ -900,7 +898,9 @@ def top_k( # - 1.0: Selected as part of the top-k items # - -1.0: Excluded from the top-k items status_column = guid.generate_guid("status") - df[status_column] = bigframes.series.Series(None, dtype=dtypes.FLOAT_DTYPE) + df[status_column] = bigframes.series.Series( + None, dtype=dtypes.FLOAT_DTYPE, session=df._session + ) num_selected = 0 while num_selected < k: diff --git a/docs/reference/bigframes.pandas/frame.rst b/docs/reference/bigframes.pandas/frame.rst index bc9f714416..4e231bd821 100644 --- a/docs/reference/bigframes.pandas/frame.rst +++ b/docs/reference/bigframes.pandas/frame.rst @@ -34,3 +34,11 @@ Struct handling :members: :inherited-members: :undoc-members: + +AI operators +^^^^^^^^^^^^ + +.. autoclass:: bigframes.operations.ai.AIAccessor + :members: + :inherited-members: + :undoc-members: \ No newline at end of file diff --git a/docs/templates/toc.yml b/docs/templates/toc.yml index b4f513b11d..b00044b087 100644 --- a/docs/templates/toc.yml +++ b/docs/templates/toc.yml @@ -45,6 +45,8 @@ uid: bigframes.operations.plotting.PlotAccessor - name: StructAccessor uid: bigframes.operations.structs.StructFrameAccessor + - name: AI + uid: bigframes.operations.ai.AIAccessor - items: - name: DataFrameGroupBy uid: bigframes.core.groupby.DataFrameGroupBy diff --git a/tests/system/large/operations/test_ai.py b/tests/system/large/operations/test_ai.py new file mode 100644 index 0000000000..04074a2ea6 --- /dev/null +++ b/tests/system/large/operations/test_ai.py @@ -0,0 +1,956 @@ +# 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 contextlib import nullcontext +from unittest.mock import patch + +import pandas as pd +import pandas.testing +import pytest + +import bigframes +from bigframes import dataframe, exceptions, series + +AI_OP_EXP_OPTION = "experiments.ai_operators" +BLOB_EXP_OPTION = "experiments.blob" +THRESHOLD_OPTION = "compute.ai_ops_confirmation_threshold" + + +def test_ai_experiment_off_raise_error(): + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with bigframes.option_context(AI_OP_EXP_OPTION, False), pytest.raises( + NotImplementedError + ): + df.ai + + +def test_filter(session, gemini_flash_model): + df = dataframe.DataFrame( + data={ + "country": ["USA", "Germany"], + "city": ["Seattle", "Berlin"], + "year": [2023, 2024], + }, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_df = df.ai.filter( + "{city} is the capital of {country} in {year}", gemini_flash_model + ).to_pandas() + + expected_df = pd.DataFrame( + {"country": ["Germany"], "city": ["Berlin"], "year": [2024]}, index=[1] + ) + pandas.testing.assert_frame_equal( + actual_df, expected_df, check_dtype=False, check_index_type=False + ) + + +def test_filter_multi_model(session, gemini_flash_model): + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + BLOB_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + df = session.from_glob_path( + "gs://bigframes-dev-testing/a_multimodel/images/*", name="image" + ) + df["prey"] = series.Series( + ["building", "cross road", "rock", "squirrel", "rabbit"], session=session + ) + result = df.ai.filter( + "The object in {image} feeds on {prey}", + gemini_flash_model, + ).to_pandas() + + assert len(result) <= len(df) + + +@pytest.mark.parametrize( + ("reply"), + [ + pytest.param("y"), + pytest.param( + "n", marks=pytest.mark.xfail(raises=exceptions.OperationAbortedError) + ), + ], +) +def test_filter_with_confirmation(session, gemini_flash_model, reply, monkeypatch): + df = dataframe.DataFrame( + data={ + "country": ["USA", "Germany"], + "city": ["Seattle", "Berlin"], + "year": [2023, 2024], + }, + session=session, + ) + monkeypatch.setattr("builtins.input", lambda: reply) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 0, + ): + df.ai.filter("{city} is the capital of {country} in {year}", gemini_flash_model) + + +def test_filter_single_column_reference(session, gemini_flash_model): + df = dataframe.DataFrame( + data={"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_df = df.ai.filter( + "{country} is in Europe", gemini_flash_model + ).to_pandas() + + expected_df = pd.DataFrame({"country": ["Germany"], "city": ["Berlin"]}, index=[1]) + pandas.testing.assert_frame_equal( + actual_df, expected_df, check_dtype=False, check_index_type=False + ) + + +@pytest.mark.parametrize( + "instruction", + [ + pytest.param( + "No column reference", + id="zero_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "{city} is in the {non_existing_column}", + id="non_existing_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "{id}", + id="invalid_type", + marks=pytest.mark.xfail(raises=TypeError), + ), + ], +) +def test_filter_invalid_instruction_raise_error(instruction, gemini_flash_model): + df = dataframe.DataFrame({"id": [1, 2], "city": ["Seattle", "Berlin"]}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df.ai.filter(instruction, gemini_flash_model) + + +def test_filter_invalid_model_raise_error(): + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]} + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(TypeError): + df.ai.filter("{city} is the capital of {country}", None) + + +def test_map(session, gemini_flash_model): + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + "gluten-free": [True, True], + }, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_df = df.ai.map( + "What is the {gluten-free} food made from {ingredient_1} and {ingredient_2}? One word only.", + "food", + gemini_flash_model, + ).to_pandas() + # Result sanitation + actual_df["food"] = actual_df["food"].str.strip().str.lower() + + expected_df = pd.DataFrame( + { + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + "gluten-free": [True, True], + "food": ["burger", "tofu"], + } + ) + pandas.testing.assert_frame_equal( + actual_df, + expected_df, + check_dtype=False, + check_index_type=False, + check_column_type=False, + ) + + +def test_map_multimodel(session, gemini_flash_model): + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + BLOB_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + df = session.from_glob_path( + "gs://bigframes-dev-testing/a_multimodel/images/*", name="image" + ) + df["scenario"] = series.Series( + ["building", "cross road", "tree", "squirrel", "rabbit"], session=session + ) + result = df.ai.map( + "What is the object in {image} combined with {scenario}? One word only.", + "object", + gemini_flash_model, + ).to_pandas() + + assert len(result) == len(df) + + +@pytest.mark.parametrize( + ("reply"), + [ + pytest.param("y"), + pytest.param( + "n", marks=pytest.mark.xfail(raises=exceptions.OperationAbortedError) + ), + ], +) +def test_map_with_confirmation(session, gemini_flash_model, reply, monkeypatch): + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + "gluten-free": [True, True], + }, + session=session, + ) + monkeypatch.setattr("builtins.input", lambda: reply) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 0, + ): + df.ai.map( + "What is the {gluten-free} food made from {ingredient_1} and {ingredient_2}? One word only.", + "food", + gemini_flash_model, + ) + + +@pytest.mark.parametrize( + "instruction", + [ + pytest.param( + "No column reference", + id="zero_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "What is the food made from {ingredient_1} and {non_existing_column}?}", + id="non_existing_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "{id}", + id="invalid_type", + marks=pytest.mark.xfail(raises=TypeError), + ), + ], +) +def test_map_invalid_instruction_raise_error(instruction, gemini_flash_model): + df = dataframe.DataFrame( + data={ + "id": [1, 2], + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + } + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df.ai.map(instruction, "food", gemini_flash_model) + + +def test_map_invalid_model_raise_error(): + df = dataframe.DataFrame( + data={ + "ingredient_1": ["Burger Bun", "Soy Bean"], + "ingredient_2": ["Beef Patty", "Bittern"], + }, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(TypeError): + df.ai.map( + "What is the food made from {ingredient_1} and {ingredient_2}? One word only.", + "food", + None, + ) + + +@pytest.mark.parametrize( + "instruction", + [ + pytest.param("{city} is in {country}", id="no_dataframe_reference"), + pytest.param("{left.city} is in {country}", id="has_left_dataframe_reference"), + pytest.param( + "{city} is in {right.country}", + id="has_right_dataframe_reference", + ), + pytest.param( + "{left.city} is in {right.country}", id="has_both_dataframe_references" + ), + ], +) +def test_join(instruction, session, gemini_flash_model): + cities = dataframe.DataFrame( + data={ + "city": ["Seattle", "Berlin"], + }, + session=session, + ) + countries = dataframe.DataFrame( + data={"country": ["USA", "UK", "Germany"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_df = cities.ai.join( + countries, + instruction, + gemini_flash_model, + ).to_pandas() + + expected_df = pd.DataFrame( + { + "city": ["Seattle", "Berlin"], + "country": ["USA", "Germany"], + } + ) + pandas.testing.assert_frame_equal( + actual_df, + expected_df, + check_dtype=False, + check_index_type=False, + check_column_type=False, + ) + + +@pytest.mark.parametrize( + ("reply"), + [ + pytest.param("y"), + pytest.param( + "n", marks=pytest.mark.xfail(raises=exceptions.OperationAbortedError) + ), + ], +) +def test_join_with_confirmation(session, gemini_flash_model, reply, monkeypatch): + cities = dataframe.DataFrame( + data={ + "city": ["Seattle", "Berlin"], + }, + session=session, + ) + countries = dataframe.DataFrame( + data={"country": ["USA", "UK", "Germany"]}, + session=session, + ) + monkeypatch.setattr("builtins.input", lambda: reply) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 0, + ): + cities.ai.join( + countries, + "{city} is in {country}", + gemini_flash_model, + ) + + +def test_self_join(session, gemini_flash_model): + animals = dataframe.DataFrame( + data={ + "animal": ["spider", "capybara"], + }, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_df = animals.ai.join( + animals, + "{left.animal} is heavier than {right.animal}", + gemini_flash_model, + ).to_pandas() + + expected_df = pd.DataFrame( + { + "animal_left": ["capybara"], + "animal_right": ["spider"], + } + ) + pandas.testing.assert_frame_equal( + actual_df, + expected_df, + check_dtype=False, + check_index_type=False, + check_column_type=False, + ) + + +@pytest.mark.parametrize( + ("instruction", "error_pattern"), + [ + ("No column reference", "No column references"), + pytest.param( + "{city} is in {continent}", r"Column .+ not found", id="non_existing_column" + ), + pytest.param( + "{city} is in {country}", + r"Ambiguous column reference: .+", + id="ambiguous_column", + ), + pytest.param( + "{right.city} is in {country}", r"Column .+ not found", id="wrong_prefix" + ), + pytest.param( + "{city} is in {right.continent}", + r"Column .+ not found", + id="prefix_on_non_existing_column", + ), + ], +) +def test_join_invalid_instruction_raise_error( + instruction, error_pattern, gemini_flash_model +): + df1 = dataframe.DataFrame( + {"city": ["Seattle", "Berlin"], "country": ["USA", "Germany"]} + ) + df2 = dataframe.DataFrame( + { + "country": ["USA", "UK", "Germany"], + "region": ["North America", "Europe", "Europe"], + } + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError, match=error_pattern): + df1.ai.join(df2, instruction, gemini_flash_model) + + +def test_join_invalid_model_raise_error(): + cities = dataframe.DataFrame({"city": ["Seattle", "Berlin"]}) + countries = dataframe.DataFrame({"country": ["USA", "UK", "Germany"]}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(TypeError): + cities.ai.join(countries, "{city} is in {country}", None) + + +@pytest.mark.parametrize( + "score_column", + [ + pytest.param(None, id="no_score_column"), + pytest.param("distance", id="has_score_column"), + ], +) +def test_search(session, text_embedding_generator, score_column): + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_result = df.ai.search( + "creatures", + "monkey", + top_k=2, + model=text_embedding_generator, + score_column=score_column, + ).to_pandas() + + expected_result = pd.Series( + ["baboons", "chimpanzee"], index=[2, 4], name="creatures" + ) + pandas.testing.assert_series_equal( + actual_result["creatures"], + expected_result, + check_dtype=False, + check_index_type=False, + ) + + if score_column is None: + assert len(actual_result.columns) == 1 + else: + assert score_column in actual_result.columns + + +@pytest.mark.parametrize( + ("reply"), + [ + pytest.param("y"), + pytest.param( + "n", marks=pytest.mark.xfail(raises=exceptions.OperationAbortedError) + ), + ], +) +def test_search_with_confirmation( + session, text_embedding_generator, reply, monkeypatch +): + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + monkeypatch.setattr("builtins.input", lambda: reply) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 0, + ): + df.ai.search( + "creatures", + "monkey", + top_k=2, + model=text_embedding_generator, + ) + + +def test_search_invalid_column_raises_error(session, text_embedding_generator): + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df.ai.search("whatever", "monkey", top_k=2, model=text_embedding_generator) + + +def test_search_invalid_model_raises_error(session): + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(TypeError): + df.ai.search("creatures", "monkey", top_k=2, model=None) + + +def test_search_invalid_top_k_raises_error(session, text_embedding_generator): + df = dataframe.DataFrame( + data={"creatures": ["salmon", "sea urchin", "baboons", "frog", "chimpanzee"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df.ai.search("creatures", "monkey", top_k=0, model=text_embedding_generator) + + +@pytest.mark.parametrize( + "score_column", + [ + pytest.param(None, id="no_score_column"), + pytest.param("distance", id="has_score_column"), + ], +) +def test_sim_join(session, text_embedding_generator, score_column): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + actual_result = df1.ai.sim_join( + df2, + left_on="creatures", + right_on="creatures", + model=text_embedding_generator, + top_k=1, + score_column=score_column, + ).to_pandas() + + expected_result = pd.DataFrame( + {"creatures": ["salmon", "cat"], "creatures_1": ["tuna", "dog"]} + ) + pandas.testing.assert_frame_equal( + actual_result[["creatures", "creatures_1"]], + expected_result, + check_dtype=False, + check_index_type=False, + ) + + if score_column is None: + assert len(actual_result.columns) == 2 + else: + assert score_column in actual_result.columns + + +@pytest.mark.parametrize( + ("reply"), + [ + pytest.param("y"), + pytest.param( + "n", marks=pytest.mark.xfail(raises=exceptions.OperationAbortedError) + ), + ], +) +def test_sim_join_with_confirmation( + session, text_embedding_generator, reply, monkeypatch +): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + monkeypatch.setattr("builtins.input", lambda: reply) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 0, + ): + df1.ai.sim_join( + df2, + left_on="creatures", + right_on="creatures", + model=text_embedding_generator, + top_k=1, + ) + + +@pytest.mark.parametrize( + ("left_on", "right_on"), + [ + pytest.param("whatever", "creatures", id="incorrect_left_column"), + pytest.param("creatures", "whatever", id="incorrect_right_column"), + ], +) +def test_sim_join_invalid_column_raises_error( + session, text_embedding_generator, left_on, right_on +): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df1.ai.sim_join( + df2, left_on=left_on, right_on=right_on, model=text_embedding_generator + ) + + +def test_sim_join_invalid_model_raises_error(session): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(TypeError): + df1.ai.sim_join(df2, left_on="creatures", right_on="creatures", model=None) + + +def test_sim_join_invalid_top_k_raises_error(session, text_embedding_generator): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df1.ai.sim_join( + df2, + left_on="creatures", + right_on="creatures", + top_k=0, + model=text_embedding_generator, + ) + + +def test_sim_join_data_too_large_raises_error(session, text_embedding_generator): + df1 = dataframe.DataFrame( + data={"creatures": ["salmon", "cat"]}, + session=session, + ) + df2 = dataframe.DataFrame( + data={"creatures": ["dog", "tuna"]}, + session=session, + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df1.ai.sim_join( + df2, + left_on="creatures", + right_on="creatures", + model=text_embedding_generator, + max_rows=1, + ) + + +@pytest.mark.parametrize( + "instruction", + [ + pytest.param( + "No column reference", + id="zero_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "{Animals}", + id="non_existing_column", + marks=pytest.mark.xfail(raises=ValueError), + ), + pytest.param( + "{Animals} and {Animals}", + id="two_columns", + marks=pytest.mark.xfail(raises=NotImplementedError), + ), + pytest.param( + "{index}", + id="preserved", + marks=pytest.mark.xfail(raises=ValueError), + ), + ], +) +def test_top_k_invalid_instruction_raise_error(instruction, gemini_flash_model): + df = dataframe.DataFrame( + { + "Animals": ["Dog", "Cat", "Bird", "Horse"], + "ID": [1, 2, 3, 4], + "index": ["a", "b", "c", "d"], + } + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ): + df.ai.top_k(instruction, model=gemini_flash_model, k=2) + + +def test_top_k_invalid_k_raise_error(gemini_flash_model): + df = dataframe.DataFrame({"Animals": ["Dog", "Cat", "Bird", "Horse"]}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 10, + ), pytest.raises(ValueError): + df.ai.top_k( + "{Animals} are more popular as pets", + gemini_flash_model, + k=0, + ) + + +@patch("builtins.input", return_value="") +def test_confirm_operation__below_threshold_do_not_confirm(mock_input): + df = dataframe.DataFrame({}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 3, + ): + df.ai._confirm_operation(1) + + mock_input.assert_not_called() + + +@patch("builtins.input", return_value="") +def test_confirm_operation__threshold_is_none_do_not_confirm(mock_input): + df = dataframe.DataFrame({}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + None, + ): + df.ai._confirm_operation(100) + + mock_input.assert_not_called() + + +@patch("builtins.input", return_value="") +def test_confirm_operation__threshold_autofail_do_not_confirm(mock_input): + df = dataframe.DataFrame({}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 1, + "compute.ai_ops_threshold_autofail", + True, + ), pytest.raises(exceptions.OperationAbortedError): + df.ai._confirm_operation(100) + + mock_input.assert_not_called() + + +@pytest.mark.parametrize( + ("reply", "expectation"), + [ + ("y", nullcontext()), + ("yes", nullcontext()), + ("", nullcontext()), + ("n", pytest.raises(exceptions.OperationAbortedError)), + ("something", pytest.raises(exceptions.OperationAbortedError)), + ], +) +def test_confirm_operation__above_threshold_confirm(reply, expectation, monkeypatch): + monkeypatch.setattr("builtins.input", lambda: reply) + df = dataframe.DataFrame({}) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 3, + ), expectation as e: + assert df.ai._confirm_operation(4) == e diff --git a/tests/system/small/operations/test_ai.py b/tests/system/small/operations/test_ai.py new file mode 100644 index 0000000000..de6ba4b86c --- /dev/null +++ b/tests/system/small/operations/test_ai.py @@ -0,0 +1,141 @@ +# 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. + + +# Note that the tests in this files uses fake models for deterministic results. +# Tests that use real LLM models are under system/large/test_ai.py + +import pandas as pd +import pandas.testing +import pytest + +import bigframes +from bigframes import dataframe, dtypes +from bigframes.ml import llm + +AI_OP_EXP_OPTION = "experiments.ai_operators" +THRESHOLD_OPTION = "compute.ai_ops_confirmation_threshold" + + +class FakeGeminiTextGenerator(llm.GeminiTextGenerator): + def __init__(self, prediction): + self.prediction = prediction + + def predict(self, *args, **kwargs): + return self.prediction + + +def test_experiment_off_raise_error(session): + df = dataframe.DataFrame( + {"country": ["USA", "Germany"], "city": ["Seattle", "Berlin"]}, session=session + ) + + with bigframes.option_context(AI_OP_EXP_OPTION, False), pytest.raises( + NotImplementedError + ): + df.ai + + +def test_filter(session): + df = dataframe.DataFrame({"col": ["A", "B"]}, session=session) + model = FakeGeminiTextGenerator( + dataframe.DataFrame( + {"ml_generate_text_llm_result": ["true", "false"]}, session=session + ), + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 50, + ): + result = df.ai.filter( + "filter {col}", + model=model, + ).to_pandas() + + pandas.testing.assert_frame_equal( + result, + pd.DataFrame({"col": ["A"]}, dtype=dtypes.STRING_DTYPE), + check_index_type=False, + ) + + +def test_map(session): + df = dataframe.DataFrame({"col": ["A", "B"]}, session=session) + model = FakeGeminiTextGenerator( + dataframe.DataFrame( + {"ml_generate_text_llm_result": ["true", "false"]}, session=session + ), + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 50, + ): + result = df.ai.map("map {col}", model=model, output_column="output").to_pandas() + + pandas.testing.assert_frame_equal( + result, + pd.DataFrame( + {"col": ["A", "B"], "output": ["true", "false"]}, dtype=dtypes.STRING_DTYPE + ), + check_index_type=False, + ) + + +def test_join(session): + left_df = dataframe.DataFrame({"col_A": ["A"]}, session=session) + right_df = dataframe.DataFrame({"col_B": ["B"]}, session=session) + model = FakeGeminiTextGenerator( + dataframe.DataFrame({"ml_generate_text_llm_result": ["true"]}, session=session), + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 50, + ): + result = left_df.ai.join( + right_df, "join {col_A} and {col_B}", model + ).to_pandas() + + pandas.testing.assert_frame_equal( + result, + pd.DataFrame({"col_A": ["A"], "col_B": ["B"]}, dtype=dtypes.STRING_DTYPE), + check_index_type=False, + ) + + +def test_top_k(session): + df = dataframe.DataFrame({"col": ["A", "B"]}, session=session) + model = FakeGeminiTextGenerator( + dataframe.DataFrame( + {"ml_generate_text_llm_result": ["Document 1"]}, session=session + ), + ) + + with bigframes.option_context( + AI_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 50, + ): + result = df.ai.top_k("top k of {col}", model, k=1).to_pandas() + + assert len(result) == 1 diff --git a/tests/system/small/operations/test_semantics.py b/tests/system/small/operations/test_semantics.py index 85777faaf6..8b520d8c03 100644 --- a/tests/system/small/operations/test_semantics.py +++ b/tests/system/small/operations/test_semantics.py @@ -122,3 +122,22 @@ def test_join(session): pd.DataFrame({"col_A": ["A"], "col_B": ["B"]}, dtype=dtypes.STRING_DTYPE), check_index_type=False, ) + + +def test_top_k(session): + df = dataframe.DataFrame({"col": ["A", "B"]}, session=session) + model = FakeGeminiTextGenerator( + dataframe.DataFrame( + {"ml_generate_text_llm_result": ["Document 1"]}, session=session + ), + ) + + with bigframes.option_context( + SEM_OP_EXP_OPTION, + True, + THRESHOLD_OPTION, + 50, + ): + result = df.semantics.top_k("top k of {col}", model, k=1).to_pandas() + + assert len(result) == 1 diff --git a/tests/unit/_config/test_experiment_options.py b/tests/unit/_config/test_experiment_options.py index 9735e494be..ce1dd0f146 100644 --- a/tests/unit/_config/test_experiment_options.py +++ b/tests/unit/_config/test_experiment_options.py @@ -27,12 +27,27 @@ def test_semantic_operators_default_false(): def test_semantic_operators_set_true_shows_warning(): options = experiment_options.ExperimentOptions() - with pytest.warns(bfe.PreviewWarning): + with pytest.warns(FutureWarning): options.semantic_operators = True assert options.semantic_operators is True +def test_ai_operators_default_false(): + options = experiment_options.ExperimentOptions() + + assert options.ai_operators is False + + +def test_ai_operators_set_true_shows_warning(): + options = experiment_options.ExperimentOptions() + + with pytest.warns(bfe.PreviewWarning): + options.ai_operators = True + + assert options.ai_operators is True + + def test_blob_default_false(): options = experiment_options.ExperimentOptions() diff --git a/tests/unit/test_dataframe.py b/tests/unit/test_dataframe.py index a6ad5e3821..8d1e77510a 100644 --- a/tests/unit/test_dataframe.py +++ b/tests/unit/test_dataframe.py @@ -89,3 +89,14 @@ def test_dataframe_to_gbq_writes_to_anonymous_dataset( destination = dataframe.to_gbq() assert destination.startswith(anonymous_dataset_id) + + +def test_dataframe_semantics_property_future_warning( + monkeypatch: pytest.MonkeyPatch, +): + dataframe = resources.create_dataframe(monkeypatch) + + with bigframes.option_context("experiments.semantic_operators", True), pytest.warns( + FutureWarning + ): + dataframe.semantics From 7e5b6a873d00162ffca3d254d3af276c5f06d866 Mon Sep 17 00:00:00 2001 From: rey-esp Date: Tue, 25 Mar 2025 14:43:58 -0500 Subject: [PATCH 22/39] feat: add Linear_Regression.global_explain() (#1446) * feat: add Linear_Regression.global_explain() * remove class_level_explain param * working global_explain() * begin adding tests * update snippet * complete snippet * failing, near complete linear model test * passing system test * Update core.py - set index to have sorted by feature * Update test_linear_model.py - remove set/set index * Update linear_model.py - fix doc section * Update conftest.py - rename penguins w global explain * Update linear_model.py - complete doc * lint * passing test and fixed expected results --- bigframes/ml/core.py | 10 ++++++ bigframes/ml/linear_model.py | 20 +++++++++++ bigframes/ml/sql.py | 6 ++++ .../linear_regression_tutorial_test.py | 25 +++++++++++++ tests/system/small/ml/conftest.py | 9 +++++ tests/system/small/ml/test_linear_model.py | 36 +++++++++++++++++++ 6 files changed, 106 insertions(+) diff --git a/bigframes/ml/core.py b/bigframes/ml/core.py index ad00ed3f2c..01917fd6d8 100644 --- a/bigframes/ml/core.py +++ b/bigframes/ml/core.py @@ -134,6 +134,16 @@ def explain_predict( ), ) + def global_explain(self, options: Mapping[str, bool]) -> bpd.DataFrame: + sql = self._model_manipulation_sql_generator.ml_global_explain( + struct_options=options + ) + return ( + self._session.read_gbq(sql) + .sort_values(by="attribution", ascending=False) + .set_index("feature") + ) + def transform(self, input_data: bpd.DataFrame) -> bpd.DataFrame: return self._apply_ml_tvf( input_data, diff --git a/bigframes/ml/linear_model.py b/bigframes/ml/linear_model.py index 46c5744a42..3774a62c0c 100644 --- a/bigframes/ml/linear_model.py +++ b/bigframes/ml/linear_model.py @@ -203,6 +203,26 @@ def predict_explain( X, options={"top_k_features": top_k_features} ) + def global_explain( + self, + ) -> bpd.DataFrame: + """ + Provide explanations for an entire linear regression model. + + .. note:: + Output matches that of the BigQuery ML.GLOBAL_EXPLAIN function. + See: https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-global-explain + + Returns: + bigframes.pandas.DataFrame: + Dataframes containing feature importance values and corresponding attributions, designed to provide a global explanation of feature influence. + """ + + if not self._bqml_model: + raise RuntimeError("A model must be fitted before predict") + + return self._bqml_model.global_explain({}) + def score( self, X: utils.ArrayType, diff --git a/bigframes/ml/sql.py b/bigframes/ml/sql.py index b662d4c22c..e89f17bcaa 100644 --- a/bigframes/ml/sql.py +++ b/bigframes/ml/sql.py @@ -312,6 +312,12 @@ def ml_explain_predict( return f"""SELECT * FROM ML.EXPLAIN_PREDICT(MODEL {self._model_ref_sql()}, ({source_sql}), {struct_options_sql})""" + def ml_global_explain(self, struct_options) -> str: + """Encode ML.GLOBAL_EXPLAIN for BQML""" + struct_options_sql = self.struct_options(**struct_options) + return f"""SELECT * FROM ML.GLOBAL_EXPLAIN(MODEL {self._model_ref_sql()}, + {struct_options_sql})""" + def ml_forecast(self, struct_options: Mapping[str, Union[int, float]]) -> str: """Encode ML.FORECAST for BQML""" struct_options_sql = self.struct_options(**struct_options) diff --git a/samples/snippets/linear_regression_tutorial_test.py b/samples/snippets/linear_regression_tutorial_test.py index e4ace53a5c..8fc1c5ad61 100644 --- a/samples/snippets/linear_regression_tutorial_test.py +++ b/samples/snippets/linear_regression_tutorial_test.py @@ -92,6 +92,31 @@ def test_linear_regression(random_model_id: str) -> None: # 3 5349.603734 [{'feature': 'island', 'attribution': 7348.877... -5320.222128 5349.603734 0.0 Gentoo penguin (Pygoscelis papua) Biscoe 46.4 15.6 221.0 5000.0 MALE # 4 4637.165037 [{'feature': 'island', 'attribution': 7348.877... -5320.222128 4637.165037 0.0 Gentoo penguin (Pygoscelis papua) Biscoe 46.1 13.2 211.0 4500.0 FEMALE # [END bigquery_dataframes_bqml_linear_predict_explain] + # [START bigquery_dataframes_bqml_linear_global_explain] + # To use the `global_explain()` function, the model must be recreated with `enable_global_explain` set to `True`. + model = LinearRegression(enable_global_explain=True) + + # The model must the be fitted before it can be saved to BigQuery and then explained. + training_data = bq_df.dropna(subset=["body_mass_g"]) + X = training_data.drop(columns=["body_mass_g"]) + y = training_data[["body_mass_g"]] + model.fit(X, y) + model.to_gbq("bqml_tutorial.penguins_model", replace=True) + + # Explain the model + explain_model = model.global_explain() + + # Expected results: + # attribution + # feature + # island 5737.315921 + # species 4073.280549 + # sex 622.070896 + # flipper_length_mm 193.612051 + # culmen_depth_mm 117.084944 + # culmen_length_mm 94.366793 + # [END bigquery_dataframes_bqml_linear_global_explain] + assert explain_model is not None assert feature_columns is not None assert label_columns is not None assert model is not None diff --git a/tests/system/small/ml/conftest.py b/tests/system/small/ml/conftest.py index 0e8489c513..2b9392f523 100644 --- a/tests/system/small/ml/conftest.py +++ b/tests/system/small/ml/conftest.py @@ -84,6 +84,15 @@ def ephemera_penguins_linear_model( return bf_model +@pytest.fixture(scope="function") +def penguins_linear_model_w_global_explain( + penguins_bqml_linear_model: core.BqmlModel, +) -> linear_model.LinearRegression: + bf_model = linear_model.LinearRegression(enable_global_explain=True) + bf_model._bqml_model = penguins_bqml_linear_model + return bf_model + + @pytest.fixture(scope="session") def penguins_logistic_model( session, penguins_logistic_model_name diff --git a/tests/system/small/ml/test_linear_model.py b/tests/system/small/ml/test_linear_model.py index da9fc8e14f..8b04d55e61 100644 --- a/tests/system/small/ml/test_linear_model.py +++ b/tests/system/small/ml/test_linear_model.py @@ -228,6 +228,42 @@ def test_to_gbq_saved_linear_reg_model_scores( ) +def test_linear_reg_model_global_explain( + penguins_linear_model_w_global_explain, new_penguins_df +): + training_data = new_penguins_df.dropna(subset=["body_mass_g"]) + X = training_data.drop(columns=["body_mass_g"]) + y = training_data[["body_mass_g"]] + penguins_linear_model_w_global_explain.fit(X, y) + global_ex = penguins_linear_model_w_global_explain.global_explain() + assert global_ex.shape == (6, 1) + expected_columns = pandas.Index(["attribution"]) + pandas.testing.assert_index_equal(global_ex.columns, expected_columns) + result = global_ex.to_pandas().drop(["attribution"], axis=1).sort_index() + expected_feature = ( + pandas.DataFrame( + { + "feature": [ + "island", + "species", + "sex", + "flipper_length_mm", + "culmen_depth_mm", + "culmen_length_mm", + ] + }, + ) + .set_index("feature") + .sort_index() + ) + pandas.testing.assert_frame_equal( + result, + expected_feature, + check_exact=False, + check_index_type=False, + ) + + def test_to_gbq_replace(penguins_linear_model, table_id_unique): penguins_linear_model.to_gbq(table_id_unique, replace=True) with pytest.raises(google.api_core.exceptions.Conflict): From 222af752c21937a673b0471194e09f9607ad1d08 Mon Sep 17 00:00:00 2001 From: Shobhit Singh Date: Tue, 25 Mar 2025 13:06:52 -0700 Subject: [PATCH 23/39] chore: set up release-please for previous major version (1.x) (#1535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: configure CI for 1.x branch (#1528) * chore: set up release-please for previous major version (1.x) * revert owlbot.py change as it already excludes release-please.yml * chore: set up release-please for previous major version (1.x) * revert owlbot.py change as it already excludes release-please.yml * Revert "chore: configure CI for 1.x branch (#1528)" This reverts commit 54e63ea395f49a29d8f5c50a0e8fc5c08194e996. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .github/release-please.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/release-please.yml b/.github/release-please.yml index 8c34d1b49f..7c2b8d9e8a 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -3,3 +3,8 @@ handleGHRelease: true extraFiles: - bigframes/version.py - third_party/bigframes_vendored/version.py + +branches: + - branch: v1 + handleGHRelease: true + releaseType: python From bdcb1e7929dc2f24c642ddb052629da394f45876 Mon Sep 17 00:00:00 2001 From: TrevorBergeron Date: Tue, 25 Mar 2025 13:35:44 -0700 Subject: [PATCH 24/39] feat: Support window partition by geo column (#1512) --- bigframes/core/compile/compiled.py | 6 ++++-- tests/system/small/geopandas/test_geoseries.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index d2fd7f3ea2..4443c495d7 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -665,9 +665,11 @@ def _join_condition( def _as_groupable(value: ibis_types.Value): - # Some types need to be converted to string to enable groupby - if value.type().is_float64() or value.type().is_geospatial(): + # Some types need to be converted to another type to enable groupby + if value.type().is_float64(): return value.cast(ibis_dtypes.str) + elif value.type().is_geospatial(): + return typing.cast(ibis_types.GeoSpatialColumn, value).as_binary() elif value.type().is_json(): return scalar_op_compiler.to_json_string(value) else: diff --git a/tests/system/small/geopandas/test_geoseries.py b/tests/system/small/geopandas/test_geoseries.py index fdd9826468..fb101dea89 100644 --- a/tests/system/small/geopandas/test_geoseries.py +++ b/tests/system/small/geopandas/test_geoseries.py @@ -289,3 +289,20 @@ def test_geo_difference_with_similar_geometry_objects(): assert expected.iloc[0].equals(bf_result.iloc[0]) assert expected.iloc[1].equals(bf_result.iloc[1]) assert expected.iloc[2].equals(bf_result.iloc[2]) + + +def test_geo_drop_duplicates(): + bf_series = bigframes.geopandas.GeoSeries( + [Point(1, 1), Point(2, 2), Point(3, 3), Point(2, 2)] + ) + + pd_series = geopandas.GeoSeries( + [Point(1, 1), Point(2, 2), Point(3, 3), Point(2, 2)] + ) + + bf_result = bf_series.drop_duplicates().to_pandas() + pd_result = pd_series.drop_duplicates() + + pd.testing.assert_series_equal( + geopandas.GeoSeries(bf_result), pd_result, check_index=False + ) From c93e7204758435b0306699d3a1332aaf522f576b Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 25 Mar 2025 14:37:55 -0700 Subject: [PATCH 25/39] fix: add deprecation warning to TextEmbeddingGenerator model, espeically gemini-1.0-X and gemini-1.5-X (#1534) --- bigframes/ml/llm.py | 8 + .../apps/synthetic_data_generation.ipynb | 849 +------------ .../bq_dataframes_llm_code_generation.ipynb | 2 +- .../bq_dataframes_llm_kmeans.ipynb | 2 +- .../bq_dataframes_llm_vector_search.ipynb | 1124 +++++++++-------- ...q_dataframes_ml_drug_name_generation.ipynb | 2 +- .../generative_ai/large_language_models.ipynb | 81 +- .../bq_dataframes_template.ipynb | 2 +- samples/snippets/gemini_model_test.py | 4 +- tests/system/load/test_llm.py | 3 +- tests/system/small/ml/conftest.py | 17 + tests/system/small/ml/test_llm.py | 56 +- 12 files changed, 715 insertions(+), 1435 deletions(-) diff --git a/bigframes/ml/llm.py b/bigframes/ml/llm.py index 0117444f16..1fd9fbc4a7 100644 --- a/bigframes/ml/llm.py +++ b/bigframes/ml/llm.py @@ -918,15 +918,23 @@ def to_gbq( return new_model.session.read_gbq_model(model_name) +@typing_extensions.deprecated( + "gemini-pro and gemini-1.5-X are going to be deprecated. Use gemini-2.0-X (https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.GeminiTextGenerator) instead. ", + category=exceptions.ApiDeprecationWarning, +) @log_adapter.class_logger class GeminiTextGenerator(base.RetriableRemotePredictor): """Gemini text generator LLM model. + .. note:: + gemini-pro and gemini-1.5-X are going to be deprecated. Use gemini-2.0-X (https://cloud.google.com/python/docs/reference/bigframes/latest/bigframes.ml.llm.GeminiTextGenerator) instead. + Args: model_name (str, Default to "gemini-pro"): The model for natural language tasks. Accepted values are "gemini-pro", "gemini-1.5-pro-preview-0514", "gemini-1.5-flash-preview-0514", "gemini-1.5-pro-001", "gemini-1.5-pro-002", "gemini-1.5-flash-001", "gemini-1.5-flash-002" and "gemini-2.0-flash-exp". Default to "gemini-pro". .. note:: + "gemini-pro" is going to be deprecated. Bigframes 2 will transition to using gemini-2.0-X. "gemini-2.0-flash-exp", "gemini-1.5-pro-preview-0514" and "gemini-1.5-flash-preview-0514" is subject to the "Pre-GA Offerings Terms" in the General Service Terms section of the Service Specific Terms(https://cloud.google.com/terms/service-terms#1). Pre-GA products and features are available "as is" and might have limited support. For more information, see the launch stage descriptions diff --git a/notebooks/apps/synthetic_data_generation.ipynb b/notebooks/apps/synthetic_data_generation.ipynb index a6e8444aac..c190f219af 100644 --- a/notebooks/apps/synthetic_data_generation.ipynb +++ b/notebooks/apps/synthetic_data_generation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -52,12 +52,12 @@ "output_type": "stream", "text": [ "Collecting faker\n", - " Downloading Faker-24.9.0-py3-none-any.whl (1.8 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.8/1.8 MB\u001b[0m \u001b[31m11.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: python-dateutil>=2.4 in /usr/local/lib/python3.10/dist-packages (from faker) (2.8.2)\n", - "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.4->faker) (1.16.0)\n", - "Installing collected packages: faker\n", - "Successfully installed faker-24.9.0\n" + " Downloading faker-37.1.0-py3-none-any.whl.metadata (15 kB)\n", + "Requirement already satisfied: tzdata in /usr/local/google/home/shuowei/src/python-bigquery-dataframes/venv/lib/python3.10/site-packages (from faker) (2024.2)\n", + "Downloading faker-37.1.0-py3-none-any.whl (1.9 MB)\n", + "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.9/1.9 MB\u001b[0m \u001b[31m55.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hInstalling collected packages: faker\n", + "Successfully installed faker-37.1.0\n" ] } ], @@ -67,11 +67,23 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": { "id": "m3q1oeJALhsG" }, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'PROJECT_ID' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[3], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mbigframes\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpandas\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mbpd\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m bpd\u001b[38;5;241m.\u001b[39moptions\u001b[38;5;241m.\u001b[39mbigquery\u001b[38;5;241m.\u001b[39mproject \u001b[38;5;241m=\u001b[39m \u001b[43mPROJECT_ID\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'PROJECT_ID' is not defined" + ] + } + ], "source": [ "import bigframes.pandas as bpd\n", "bpd.options.bigquery.project = PROJECT_ID" @@ -95,32 +107,11 @@ "id": "lIYdn1woOS1n", "outputId": "be474338-44c2-4ce0-955e-d525b8b9c84b" }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.10/dist-packages/bigframes/session/__init__.py:1907: UserWarning: No explicit location is set, so using location US for the session.\n", - " return Session(context)\n" - ] - }, - { - "data": { - "text/html": [ - "Query job 3e8423da-737c-42e2-a3d2-d2180ca18579 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from bigframes.ml.llm import GeminiTextGenerator\n", "\n", - "model = GeminiTextGenerator()" + "model = GeminiTextGenerator(model_name=\"gemini-1.5-flash-002\")" ] }, { @@ -141,77 +132,7 @@ "id": "SSR-lLScLa95", "outputId": "cbaec34e-6fa6-45b4-e54a-f11ca06b61e1" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job d651d0bf-300c-4b1d-9e3c-03310b71287c is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job c67b9bb9-2f3e-4b9e-b680-0b7b6e9d2279 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", - "
prompt
0Write python code to generate a pandas datafra...
\n", - "

1 rows × 1 columns

\n", - "
[1 rows x 1 columns in total]" - ], - "text/plain": [ - " prompt\n", - "0 Write python code to generate a pandas datafra...\n", - "\n", - "[1 rows x 1 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "prompt = \"\"\"\\\n", "Write python code to generate a pandas dataframe based on the requirements:\n", @@ -248,73 +169,7 @@ "id": "miDe3K4GNvOo", "outputId": "f2039e80-5ad7-4551-f8b2-7ef714a89d63" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job d5c0725d-9070-4712-adfd-8a9bd86eefc3 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 4eb581a3-7f97-411a-bee1-91e8c150cef4 is DONE. 8 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job f3d5503d-a3e7-49ce-b985-5ffbdbd856e3 is DONE. 2 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 8ef76041-f077-4a05-bc03-63e6983ef853 is DONE. 332 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "import pandas as pd\n", - "from faker import Faker\n", - "\n", - "fake = Faker('es_ES')\n", - "result_df = pd.DataFrame({\n", - " 'Name': [fake.name() for _ in range(100)],\n", - " 'Age': [fake.random_int(min=18, max=65) for _ in range(100)],\n", - " 'Gender': [fake.random_element(elements=['Male', 'Female', 'Non-binary']) for _ in range(100)]\n", - "})\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "max_tries = 5\n", "for i in range(max_tries):\n", @@ -366,342 +221,7 @@ "id": "GODcPwX2PBEu", "outputId": "dec4c872-c464-49e4-cd7f-9442fc977d18" }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "summary": "{\n \"name\": \"execution_context\",\n \"rows\": 100,\n \"fields\": [\n {\n \"column\": \"Name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 100,\n \"samples\": [\n \"Renata Pla Cases\",\n \"Guiomar Carnero-Paz\",\n \"Luciano Garmendia\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Age\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 13,\n \"min\": 18,\n \"max\": 64,\n \"num_unique_values\": 39,\n \"samples\": [\n 56,\n 31,\n 34\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Gender\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"Male\",\n \"Non-binary\",\n \"Female\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", - "type": "dataframe" - }, - "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", - "
NameAgeGender
0Pastora Acuña Company21Male
1León Reig-Salom39Non-binary
2Aura Tomás Llobet30Female
3Vicente Correa Palomar64Female
4Benito del Fuster34Female
............
95Eduardo Cabrera27Non-binary
96Nazaret de Izaguirre40Non-binary
97Manuela Agullo Bustamante27Female
98Eugenio Mateo Naranjo Blazquez36Non-binary
99Heriberto Vicens Baeza53Female
\n", - "

100 rows × 3 columns

\n", - "
\n", - "
\n", - "\n", - "
\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "
\n", - "\n", - "\n", - "
\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "
\n", - "\n", - "
\n", - "
\n" - ], - "text/plain": [ - " Name Age Gender\n", - "0 Pastora Acuña Company 21 Male\n", - "1 León Reig-Salom 39 Non-binary\n", - "2 Aura Tomás Llobet 30 Female\n", - "3 Vicente Correa Palomar 64 Female\n", - "4 Benito del Fuster 34 Female\n", - ".. ... ... ...\n", - "95 Eduardo Cabrera 27 Non-binary\n", - "96 Nazaret de Izaguirre 40 Non-binary\n", - "97 Manuela Agullo Bustamante 27 Female\n", - "98 Eugenio Mateo Naranjo Blazquez 36 Non-binary\n", - "99 Heriberto Vicens Baeza 53 Female\n", - "\n", - "[100 rows x 3 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "execution_context = {}\n", "exec(code, execution_context)\n", @@ -726,21 +246,7 @@ "id": "n-BsGciNqSwU", "outputId": "996e5639-a49c-4542-a0dc-ede450e0eb6d" }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'projects/bigframes-dev/locations/us-central1/functions/bigframes-19f2f35637098969770261a2974bef32'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "@bpd.remote_function([int], str, packages=['faker', 'pandas'])\n", "def data_generator(id):\n", @@ -770,20 +276,7 @@ "id": "Odkmev9nsYqA", "outputId": "4aa7a1fd-0c0d-4412-f326-a20e19f583b5" }, - "outputs": [ - { - "data": { - "text/html": [ - "Load job 40b9c3a8-27fc-40a8-9edf-4aa2e0fec332 is DONE. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "desired_num_rows = 1_000_000 # 1 million rows\n", "batch_size = 100 # used in the prompt\n", @@ -803,20 +296,7 @@ "id": "UyBhlJFVsmQC", "outputId": "29748df5-673b-4320-bb1f-53abaace3b81" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 9dd49b50-2dbf-4351-b9ad-b17aeb627caf is DONE. 240.0 kB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "df[\"json_data\"] = df[\"row_id\"].apply(data_generator)" ] @@ -839,262 +319,7 @@ "id": "6p3eM21qvRvy", "outputId": "333f4e49-a555-4d2f-b527-02142782b3a7" }, - "outputs": [ - { - "data": { - "text/html": [ - "Query job 3f8d2133-b01d-402d-a731-79592810ca1c is DONE. 63.7 MB processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 4a613aa3-6323-4914-8e34-93323885d458 is DONE. 0 Bytes processed. Open Job" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Query job 0deb03be-725b-40b4-a7a1-1023b0477f35 is DONE. 40.1 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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
NameAgeGender
0Eloy Santiago-Aragón31Male
1Amanda Mata Abril20Non-binary
2Danilo Velázquez Salcedo58Male
3Leyre Alba España61Female
4Paulina Amores Pastor41Male
5Jorge Cuadrado Mena50Female
6Chucho Catalán36Non-binary
7Vidal Benavente Lerma38Male
8Clementina Álamo32Female
9Petrona Roselló-Valls61Male
10Luís Camilo Sastre Marin45Male
11Gil Baudelio Carbajo Ordóñez58Non-binary
12David del Donoso44Female
13Dolores Arnau Ros21Non-binary
14Febe de León46Non-binary
15Ariadna Almazán34Female
16Blas Serna Aguiló24Non-binary
17Paulino Barreda Almeida59Female
18Eligio Valcárcel Tormo35Non-binary
19Toño Amador Torres Portillo48Female
20Florencia del Bejarano65Non-binary
21Clímaco Andreu Gómez18Male
22Xiomara Dominguez Solana35Female
23Leire Castilla Borrego19Non-binary
24Angelita Garmendia Carpio21Non-binary
\n", - "

25 rows × 3 columns

\n", - "
[1000000 rows x 3 columns in total]" - ], - "text/plain": [ - " Name Age Gender\n", - "0 Eloy Santiago-Aragón 31 Male\n", - "1 Amanda Mata Abril 20 Non-binary\n", - "2 Danilo Velázquez Salcedo 58 Male\n", - "3 Leyre Alba España 61 Female\n", - "4 Paulina Amores Pastor 41 Male\n", - "5 Jorge Cuadrado Mena 50 Female\n", - "6 Chucho Catalán 36 Non-binary\n", - "7 Vidal Benavente Lerma 38 Male\n", - "8 Clementina Álamo 32 Female\n", - "9 Petrona Roselló-Valls 61 Male\n", - "10 Luís Camilo Sastre Marin 45 Male\n", - "11 Gil Baudelio Carbajo Ordóñez 58 Non-binary\n", - "12 David del Donoso 44 Female\n", - "13 Dolores Arnau Ros 21 Non-binary\n", - "14 Febe de León 46 Non-binary\n", - "15 Ariadna Almazán 34 Female\n", - "16 Blas Serna Aguiló 24 Non-binary\n", - "17 Paulino Barreda Almeida 59 Female\n", - "18 Eligio Valcárcel Tormo 35 Non-binary\n", - "19 Toño Amador Torres Portillo 48 Female\n", - "20 Florencia del Bejarano 65 Non-binary\n", - "21 Clímaco Andreu Gómez 18 Male\n", - "22 Xiomara Dominguez Solana 35 Female\n", - "23 Leire Castilla Borrego 19 Non-binary\n", - "24 Angelita Garmendia Carpio 21 Non-binary\n", - "...\n", - "\n", - "[1000000 rows x 3 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "sql = f\"\"\"\n", "WITH T0 AS ({df.sql}),\n", @@ -1126,6 +351,18 @@ "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.10.15" } }, "nbformat": 4, diff --git a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb index db51afd412..88633f8635 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_code_generation.ipynb @@ -430,7 +430,7 @@ "source": [ "from bigframes.ml.llm import GeminiTextGenerator\n", "\n", - "model = GeminiTextGenerator()" + "model = GeminiTextGenerator(model_name=\"gemini-1.5-flash-002\")" ] }, { diff --git a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb index 254ac65358..31a47ea424 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_kmeans.ipynb @@ -1614,7 +1614,7 @@ "source": [ "from bigframes.ml.llm import GeminiTextGenerator\n", "\n", - "q_a_model = GeminiTextGenerator()" + "q_a_model = GeminiTextGenerator(model_name=\"gemini-1.5-flash-002\")" ] }, { diff --git a/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb b/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb index 20d5b4161d..a15209aae4 100644 --- a/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb +++ b/notebooks/generative_ai/bq_dataframes_llm_vector_search.ipynb @@ -61,14 +61,14 @@ }, { "cell_type": "markdown", + "metadata": { + "id": "vFMjpPBo9aVv" + }, "source": [ "**Author:** Sudipto Guha (Google)\n", "\n", "**Last updated:** March 16th 2025" - ], - "metadata": { - "id": "vFMjpPBo9aVv" - } + ] }, { "cell_type": "markdown", @@ -136,6 +136,9 @@ }, { "cell_type": "markdown", + "metadata": { + "id": "GqLjnm1hsKGU" + }, "source": [ "## Setup & initialization\n", "\n", @@ -144,10 +147,7 @@ "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)" - ], - "metadata": { - "id": "GqLjnm1hsKGU" - } + ] }, { "cell_type": "markdown", @@ -198,17 +198,17 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "id": "b8bKCfIiooEV", "executionInfo": { + "elapsed": 2, "status": "ok", "timestamp": 1742191597773, - "user_tz": -480, - "elapsed": 2, "user": { "displayName": "", "userId": "" - } - } + }, + "user_tz": -480 + }, + "id": "b8bKCfIiooEV" }, "outputs": [], "source": [ @@ -284,23 +284,23 @@ "colab": { "base_uri": "https://localhost:8080/" }, - "id": "j3lmnsh7ooEW", "executionInfo": { + "elapsed": 2, "status": "ok", "timestamp": 1742191608487, - "user_tz": -480, - "elapsed": 2, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "j3lmnsh7ooEW", "outputId": "eb68daf5-5558-487a-91d2-4b4f9e476da0" }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "WARNING: google.colab.auth.authenticate_user() is not supported in Colab Enterprise.\n" ] @@ -342,17 +342,17 @@ "cell_type": "code", "execution_count": 33, "metadata": { - "id": "R7STCS8xB5d2", "executionInfo": { + "elapsed": 947, "status": "ok", "timestamp": 1742195413800, - "user_tz": -480, - "elapsed": 947, "user": { "displayName": "", "userId": "" - } - } + }, + "user_tz": -480 + }, + "id": "R7STCS8xB5d2" }, "outputs": [], "source": [ @@ -385,33 +385,33 @@ }, { "cell_type": "markdown", - "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." - ], "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", - "source": [ - "bf.options.bigquery.ordering_mode = \"partial\"" - ], + "execution_count": 4, "metadata": { - "id": "9Gil1Oaas7KA", "executionInfo": { + "elapsed": 2, "status": "ok", "timestamp": 1742191620533, - "user_tz": -480, - "elapsed": 2, "user": { "displayName": "", "userId": "" - } - } + }, + "user_tz": -480 + }, + "id": "9Gil1Oaas7KA" }, - "execution_count": 4, - "outputs": [] + "outputs": [], + "source": [ + "bf.options.bigquery.ordering_mode = \"partial\"" + ] }, { "cell_type": "markdown", @@ -435,26 +435,26 @@ "cell_type": "code", "execution_count": 17, "metadata": { - "id": "zDSwoBo1CU3G", "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { + "elapsed": 468, "status": "ok", "timestamp": 1742192516923, - "user_tz": -480, - "elapsed": 468, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "zDSwoBo1CU3G", "outputId": "83edbc2f-5a23-407b-8890-f968eb31be44" }, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "/usr/local/lib/python3.10/dist-packages/IPython/core/interactiveshell.py:3553: UserWarning: \u001b[93mReading cached table from 2025-03-17 06:07:09.526507+00:00 to avoid\n", "incompatibilies with previous reads of this table. To read the latest\n", @@ -472,35 +472,35 @@ "cell_type": "code", "execution_count": 18, "metadata": { - "id": "tYDoaKgJChiq", "colab": { "base_uri": "https://localhost:8080/", "height": 34 }, "executionInfo": { + "elapsed": 6697, "status": "ok", "timestamp": 1742192524632, - "user_tz": -480, - "elapsed": 6697, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "tYDoaKgJChiq", "outputId": "9174da29-a051-4a99-e38f-6a2b09cfe4e9" }, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job 6f15ad71-cc7b-49c1-90e9-274bea7afbb9 is DONE. 477.4 GB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -514,105 +514,33 @@ }, { "cell_type": "code", - "source": [ - "## take a look at the sample dataset\n", - "\n", - "publications.head(5)" - ], + "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 556 }, - "id": "XmqdJInztzPl", "executionInfo": { + "elapsed": 6, "status": "ok", "timestamp": 1742191801044, - "user_tz": -480, - "elapsed": 6, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "XmqdJInztzPl", "outputId": "ae05f3a6-edeb-423a-c061-c416717e1ec5" }, - "execution_count": 11, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": [ - " publication_number title \\\n", - "0 AU-338190-S Compressor wheel \n", - "1 CN-100525651-C Method for processing egg products \n", - "2 TW-I725505-B Improved carbon molecular sieve adsorbent \n", - "3 EP-0248026-B1 A system for supplying strip to a processing line \n", - "4 MY-135762-A Method for producing acrylic acid \n", - "\n", - " title_translated abstract \\\n", - "0 False Newness and distinctiveness is claimed in the ... \n", - "1 False The invention discloses a processing method of... \n", - "2 False Disclosed herein are rapid cycle pressure swin... \n", - "3 False A system (10) for supplying strip material (S)... \n", - "4 False A PROCESS FOR THE FRACTIONAL CONDENSATION OF A... \n", - "\n", - " abstract_translated cpc \\\n", - "0 False [] \n", - "1 False [] \n", - "2 False [{'code': 'B01D2253/116', 'inventive': False, ... \n", - "3 False [{'code': 'B65H2701/37', 'inventive': False, '... \n", - "4 False [{'code': 'C07C51/50', 'inventive': True, 'fir... \n", - "\n", - " cpc_low \\\n", - "0 [] \n", - "1 [] \n", - "2 ['B01D2253/116' 'B01D2253/10' 'B01D2253/00' 'B... \n", - "3 ['B65H2701/37' 'B65H2701/30' 'B65H2701/00' 'B6... \n", - "4 ['C07C51/50' 'C07C51/42' 'C07C51/00' 'C07C' 'C... \n", - "\n", - " cpc_inventive_low \\\n", - "0 [] \n", - "1 [] \n", - "2 ['B01D2253/116' 'B01D2253/10' 'B01D2253/00' 'B... \n", - "3 ['B65H2701/37' 'B65H2701/30' 'B65H2701/00' 'B6... \n", - "4 ['C07C51/50' 'C07C51/42' 'C07C51/00' 'C07C' 'C... \n", - "\n", - " top_terms \\\n", - "0 ['compressor wheel' 'newness' 'distinctiveness... \n", - "1 ['egg' 'processing method' 'egg body' 'pack' '... \n", - "2 ['swing adsorption' 'pressure swing' 'molecula... \n", - "3 ['strip material' 'assembly' 'coil' 'take' 'pr... \n", - "4 ['acrylic acid' 'producing acrylic' 'stabilize... \n", - "\n", - " similar \\\n", - "0 [{'publication_number': 'AU-338190-S', 'applic... \n", - "1 [{'publication_number': 'CN-101396133-B', 'app... \n", - "2 [{'publication_number': 'EP-1867379-B1', 'appl... \n", - "3 [{'publication_number': 'EP-0248026-B1', 'appl... \n", - "4 [{'publication_number': 'SG-157371-A1', 'appli... \n", - "\n", - " url country \\\n", - "0 https://patents.google.com/patent/AU338190S Australia \n", - "1 https://patents.google.com/patent/CN100525651C China \n", - "2 https://patents.google.com/patent/TWI725505B Taiwan \n", - "3 https://patents.google.com/patent/EP0248026B1 European Patent Office \n", - "4 https://patents.google.com/patent/MY135762A Malaysia \n", - "\n", - " publication_description cited_by \\\n", - "0 Design [] \n", - "1 Granted Patent [] \n", - "2 Granted Patent or patent of addition [] \n", - "3 Granted patent [] \n", - "4 Granted patent / Utility model [] \n", - "\n", - " embedding_v1 \n", - "0 [ 5.2067090e-02 -1.5462303e-01 -1.3415462e-01 ... \n", - "1 [-0.05154578 -0.00437102 0.01365495 -0.168424... \n", - "2 [ 0.0163008 -0.20972364 0.02052403 -0.003073... \n", - "3 [-0.04377723 0.04111805 -0.0929429 0.043924... \n", - "4 [ 0.10407669 0.01262973 -0.22623734 -0.171453... " - ], + "application/vnd.google.colaboratory.intrinsic+json": { + "repr_error": "Function 'unique' has no kernel matching input types (list not null>>)", + "type": "dataframe", + "variable_name": "publications" + }, "text/html": [ "\n", "
\n", @@ -955,15 +883,87 @@ "
\n", " \n" ], - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "dataframe", - "variable_name": "publications", - "repr_error": "Function 'unique' has no kernel matching input types (list not null>>)" - } + "text/plain": [ + " publication_number title \\\n", + "0 AU-338190-S Compressor wheel \n", + "1 CN-100525651-C Method for processing egg products \n", + "2 TW-I725505-B Improved carbon molecular sieve adsorbent \n", + "3 EP-0248026-B1 A system for supplying strip to a processing line \n", + "4 MY-135762-A Method for producing acrylic acid \n", + "\n", + " title_translated abstract \\\n", + "0 False Newness and distinctiveness is claimed in the ... \n", + "1 False The invention discloses a processing method of... \n", + "2 False Disclosed herein are rapid cycle pressure swin... \n", + "3 False A system (10) for supplying strip material (S)... \n", + "4 False A PROCESS FOR THE FRACTIONAL CONDENSATION OF A... \n", + "\n", + " abstract_translated cpc \\\n", + "0 False [] \n", + "1 False [] \n", + "2 False [{'code': 'B01D2253/116', 'inventive': False, ... \n", + "3 False [{'code': 'B65H2701/37', 'inventive': False, '... \n", + "4 False [{'code': 'C07C51/50', 'inventive': True, 'fir... \n", + "\n", + " cpc_low \\\n", + "0 [] \n", + "1 [] \n", + "2 ['B01D2253/116' 'B01D2253/10' 'B01D2253/00' 'B... \n", + "3 ['B65H2701/37' 'B65H2701/30' 'B65H2701/00' 'B6... \n", + "4 ['C07C51/50' 'C07C51/42' 'C07C51/00' 'C07C' 'C... \n", + "\n", + " cpc_inventive_low \\\n", + "0 [] \n", + "1 [] \n", + "2 ['B01D2253/116' 'B01D2253/10' 'B01D2253/00' 'B... \n", + "3 ['B65H2701/37' 'B65H2701/30' 'B65H2701/00' 'B6... \n", + "4 ['C07C51/50' 'C07C51/42' 'C07C51/00' 'C07C' 'C... \n", + "\n", + " top_terms \\\n", + "0 ['compressor wheel' 'newness' 'distinctiveness... \n", + "1 ['egg' 'processing method' 'egg body' 'pack' '... \n", + "2 ['swing adsorption' 'pressure swing' 'molecula... \n", + "3 ['strip material' 'assembly' 'coil' 'take' 'pr... \n", + "4 ['acrylic acid' 'producing acrylic' 'stabilize... \n", + "\n", + " similar \\\n", + "0 [{'publication_number': 'AU-338190-S', 'applic... \n", + "1 [{'publication_number': 'CN-101396133-B', 'app... \n", + "2 [{'publication_number': 'EP-1867379-B1', 'appl... \n", + "3 [{'publication_number': 'EP-0248026-B1', 'appl... \n", + "4 [{'publication_number': 'SG-157371-A1', 'appli... \n", + "\n", + " url country \\\n", + "0 https://patents.google.com/patent/AU338190S Australia \n", + "1 https://patents.google.com/patent/CN100525651C China \n", + "2 https://patents.google.com/patent/TWI725505B Taiwan \n", + "3 https://patents.google.com/patent/EP0248026B1 European Patent Office \n", + "4 https://patents.google.com/patent/MY135762A Malaysia \n", + "\n", + " publication_description cited_by \\\n", + "0 Design [] \n", + "1 Granted Patent [] \n", + "2 Granted Patent or patent of addition [] \n", + "3 Granted patent [] \n", + "4 Granted patent / Utility model [] \n", + "\n", + " embedding_v1 \n", + "0 [ 5.2067090e-02 -1.5462303e-01 -1.3415462e-01 ... \n", + "1 [-0.05154578 -0.00437102 0.01365495 -0.168424... \n", + "2 [ 0.0163008 -0.20972364 0.02052403 -0.003073... \n", + "3 [-0.04377723 0.04111805 -0.0929429 0.043924... \n", + "4 [ 0.10407669 0.01262973 -0.22623734 -0.171453... " + ] }, + "execution_count": 11, "metadata": {}, - "execution_count": 11 + "output_type": "execute_result" } + ], + "source": [ + "## take a look at the sample dataset\n", + "\n", + "publications.head(5)" ] }, { @@ -979,35 +979,35 @@ "cell_type": "code", "execution_count": 13, "metadata": { - "id": "li38q8FzDDMu", "colab": { "base_uri": "https://localhost:8080/", "height": 34 }, "executionInfo": { + "elapsed": 4528, "status": "ok", "timestamp": 1742192047236, - "user_tz": -480, - "elapsed": 4528, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "li38q8FzDDMu", "outputId": "b8c1bd38-b484-4f71-bd38-927c8677d0c5" }, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job 127fb090-1c9e-4d7a-acdd-86f077a87b07 is DONE. 0 Bytes processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -1018,64 +1018,53 @@ }, { "cell_type": "code", - "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" - ], + "execution_count": 19, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 139 }, - "id": "b5HHZob_u61B", "executionInfo": { + "elapsed": 126632, "status": "ok", "timestamp": 1742192656608, - "user_tz": -480, - "elapsed": 126632, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "b5HHZob_u61B", "outputId": "c9ecc5fd-5d11-4fd8-f59b-9dce4e12e371" }, - "execution_count": 19, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Load job b8079d70-7d99-4198-898f-2921915f305f is DONE. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job 17338b11-420c-4d3d-bd55-0bba1247f705 is DONE. 8.9 MB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "/usr/local/lib/python3.10/dist-packages/bigframes/core/array_value.py:114: PreviewWarning: \u001b[93mJSON column interpretation as a custom PyArrow extention in\n", "`db_dtypes` is a preview feature and subject to change.\u001b[0m\n", @@ -1083,59 +1072,67 @@ ] }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job ebf3eb36-3199-4551-ad07-5fa5abb200be is DONE. 20.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job 9e9c5aae-9045-4750-a34e-c98493369a90 is DONE. 20.0 kB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } + ], + "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", - "source": [ - "embedding.head(5)" - ], + "execution_count": 20, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 464 }, - "id": "OIT5FbqAwqG5", "executionInfo": { + "elapsed": 6715, "status": "ok", "timestamp": 1742192727525, - "user_tz": -480, - "elapsed": 6715, "user": { "displayName": "", "userId": "" - } + }, + "user_tz": -480 }, + "id": "OIT5FbqAwqG5", "outputId": "d04c994a-a0c8-44b0-e897-d871036eeb1f" }, - "execution_count": 20, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "/usr/local/lib/python3.10/dist-packages/bigframes/core/array_value.py:238: AmbiguousWindowWarning: \u001b[93mWindow ordering may be ambiguous, this can cause unstable results.\u001b[0m\n", " warnings.warn(msg, bfe.AmbiguousWindowWarning)\n", @@ -1144,63 +1141,31 @@ ] }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job 1bc3517f-df67-456c-8d31-14a6432b8629 is DONE. 70.4 MB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "Query job ae92602b-0eab-437f-a02d-102a4defa99a is DONE. 31.3 kB processed. Open Job" + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "execute_result", "data": { - "text/plain": [ - " publication_number title \\\n", - "5753 HN-1996000102-A NEW PESTICIDES \n", - "8115 AU-325874-S Baby sling \n", - "5415 AU-2016256863-A1 Microbial compositions and methods for denitri... \n", - "8886 FR-2368509-A1 NEW DEODORANTS OR FRESHENERS AND COMPOSITIONS ... \n", - "5661 US-2006051255-A1 Gas generator \n", - "\n", - " content \\\n", - "5753 THE PRESENT INVENTION REFERS TO \n", - "8115 Adjustable baby sling with velcro. \n", - "5415 The present invention provides compositions an... \n", - "8886 Polyanionic polyamide salts comprising a conca... \n", - "5661 A gas generator insulated by a vacuum-jacket v... \n", - "\n", - " ml_generate_embedding_result \\\n", - "5753 [-0.02709213 0.0366395 0.03931784 -0.003942... \n", - "8115 [ 6.44167811e-02 -2.01051459e-02 -3.39564607e-... \n", - "5415 [-5.90537786e-02 2.38401629e-03 7.22754598e-... \n", - "8886 [-3.44522446e-02 5.64815439e-02 -1.35829514e-... \n", - "5661 [-1.50892800e-02 6.56989636e-03 2.34969519e-... \n", - "\n", - " ml_generate_embedding_status \n", - "5753 \n", - "8115 \n", - "5415 \n", - "8886 \n", - "5661 \n", - "\n", - "[5 rows x 5 columns]" - ], "text/html": [ "
\n", "